Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
bb5afa3
feat: add persist.report output to CLI
BioPhoton Aug 8, 2025
b992caf
fix: revert changes
BioPhoton Aug 8, 2025
9b9616e
Merge remote-tracking branch 'origin/main' into add-caching-options-t…
BioPhoton Aug 11, 2025
ccfc1df
fix: adjust models
BioPhoton Aug 11, 2025
894cf68
test: adjust e2e tests for caching
BioPhoton Aug 11, 2025
26d5f7b
test: add int tests for caching
BioPhoton Aug 11, 2025
2688343
test: fix int tests
BioPhoton Aug 11, 2025
8f421a6
test: add e2e tests
BioPhoton Aug 11, 2025
30e904c
docs: add docs
BioPhoton Aug 11, 2025
e8ac86f
fix: fix lint violation
BioPhoton Aug 11, 2025
f510678
docs: update Nx example
BioPhoton Aug 12, 2025
d5e9fab
docs: update caching examples
BioPhoton Aug 12, 2025
44f7b7c
Merge remote-tracking branch 'origin/main' into add-caching-options-t…
BioPhoton Aug 12, 2025
f9d63b8
Update packages/cli/src/lib/implementation/core-config.options.ts
BioPhoton Aug 12, 2025
b2e4fac
Update packages/cli/README.md
BioPhoton Aug 12, 2025
fd8c37e
Update packages/cli/docs/nx-caching.md
BioPhoton Aug 12, 2025
bf1718c
Update packages/cli/docs/nx-caching.md
BioPhoton Aug 12, 2025
caf8053
Update packages/cli/docs/turbo-caching.md
BioPhoton Aug 12, 2025
f3b9223
Update packages/cli/docs/nx-caching.md
BioPhoton Aug 12, 2025
531bedb
fix: update help output with caching options
BioPhoton Aug 12, 2025
520faa5
docs: fix typos
BioPhoton Aug 12, 2025
3d1c1cd
docs: adjust command example
BioPhoton Aug 12, 2025
4373c4c
docs: adjust command example
BioPhoton Aug 12, 2025
121ba4e
docs: adjust config example
BioPhoton Aug 12, 2025
0b52c07
Update packages/cli/docs/nx-caching.md
BioPhoton Aug 12, 2025
5fb3d87
docs: options docs
BioPhoton Aug 12, 2025
fdcd289
docs: adjust nx targets
BioPhoton Aug 12, 2025
9e6702a
docs: adjust nx targets 2
BioPhoton Aug 12, 2025
005d49e
Update packages/cli/docs/turbo-caching.md
BioPhoton Aug 12, 2025
ba7f967
Update packages/cli/docs/turbo-caching.md
BioPhoton Aug 12, 2025
76e3f86
docs: adjust turbo targets 1
BioPhoton Aug 12, 2025
b80b659
docs: add category filter
BioPhoton Aug 12, 2025
2fede24
docs: fix typo
BioPhoton Aug 12, 2025
98dedef
docs: fix target outputs
BioPhoton Aug 12, 2025
612dcb8
docs: fix turbo docs
BioPhoton Aug 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions e2e/cli-e2e/tests/__snapshots__/help.e2e.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,14 @@ Upload Options:
[string]

Options:
--version Show version [boolean]
-h, --help Show help [boolean]
--version Show version [boolean]
--cache Cache runner outputs (both read and write)
[boolean]
--cache.read Read runner-output.json to file system
[boolean]
--cache.write Write runner-output.json to file system
[boolean]
-h, --help Show help [boolean]

Examples:
code-pushup Run collect followed by upload based
Expand Down
25 changes: 24 additions & 1 deletion e2e/cli-e2e/tests/collect.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import {
TEST_OUTPUT_DIR,
teardownTestFolder,
} from '@code-pushup/test-utils';
import { executeProcess, readTextFile } from '@code-pushup/utils';
import { executeProcess, readJsonFile, readTextFile } from '@code-pushup/utils';
import { dummyPluginSlug } from '../mocks/fixtures/dummy-setup/dummy.plugin';

describe('CLI collect', () => {
const dummyPluginTitle = 'Dummy Plugin';
Expand Down Expand Up @@ -61,6 +62,28 @@ describe('CLI collect', () => {
expect(md).toContain(dummyAuditTitle);
});

it('should write runner outputs if --cache is given', async () => {
const { code } = await executeProcess({
command: 'npx',
args: ['@code-pushup/cli', '--no-progress', 'collect', '--cache'],
cwd: dummyDir,
});

expect(code).toBe(0);

await expect(
readJsonFile(
path.join(dummyOutputDir, dummyPluginSlug, 'runner-output.json'),
),
).resolves.toStrictEqual([
{
slug: 'dummy-audit',
score: 0.3,
value: 3,
},
]);
});

it('should print report summary to stdout', async () => {
const { code, stdout } = await executeProcess({
command: 'npx',
Expand Down
11 changes: 11 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,8 @@ Each example is fully tested to demonstrate best practices for plugin testing as
| **`--upload.project`** | `string` | n/a | Project slug from portal. |
| **`--upload.server`** | `string` | n/a | URL to your portal server. |
| **`--upload.apiKey`** | `string` | n/a | API key for the portal server. |
| **`--cache.read`** | `boolean` | `false` | If plugin audit outputs should be read from file system cache. |
| **`--cache.write`** | `boolean` | `false` | If plugin audit outputs should be written to file system cache. |
| **`--onlyPlugins`** | `string[]` | `[]` | Only run the specified plugins. Applicable to all commands except `upload`. |
| **`--skipPlugins`** | `string[]` | `[]` | Skip the specified plugins. Applicable to all commands except `upload`. |

Expand Down Expand Up @@ -326,3 +328,12 @@ In addition to the [Common Command Options](#common-command-options), the follow
| Option | Required | Type | Description |
| ------------- | :------: | ---------- | --------------------------------- |
| **`--files`** | yes | `string[]` | List of `report-diff.json` paths. |

## Caching

The CLI supports caching to speed up subsequent runs and is compatible with Nx and turborepo.

Depending on your strategy, you can cache the generated reports files or plugin runner output.
For fine-grained caching, we suggest caching plugin runner output.

The detailed example for [Nx caching](./docs/nx-caching.md) and [Turborepo caching](./docs/turbo-caching.md) is available in the docs.
131 changes: 131 additions & 0 deletions packages/cli/docs/nx-caching.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Caching Example Nx

To cache plugin runner output, you can use the `--cache.write` and `--cache.read` options in combination with `--onlyPlugins` and `--persist.skipReports` command options.

## `{projectRoot}/code-pushup.config.ts`

```ts
import coveragePlugin from '@code-pushup/coverage-plugin';
import jsPackagesPlugin from '@code-pushup/js-packages-plugin';
import type { CoreConfig } from '@code-pushup/models';

export default {
plugins: [
await coveragePlugin({
reports: ['coverage/lcov.info'],
coverageTypes: ['function', 'branch', 'line'],
}),
await jsPackagesPlugin(),
],
upload: {
server: 'https://portal.code-pushup.dev/api',
organization: 'my-org',
project: 'lib-a',
apiKey: process.env.CP_API_KEY,
},
} satisfies CoreConfig;
```

## `{projectRoot}/package.json`

```json
{
"name": "lib-a",
"targets": {
"int-test": {
"cache": true,
"outputs": ["{options.coverage.reportsDirectory}"],
"executor": "@nx/vite:test",
"options": {
"configFile": "packages/lib-a/vitest.int.config.ts",
"coverage.reportsDirectory": "{projectRoot}/coverage/int-test"
}
},
"unit-test": {
"cache": true,
"outputs": ["{options.coverage.reportsDirectory}"],
"executor": "@nx/vite:test",
"options": {
"configFile": "packages/lib-a/vitest.unit.config.ts",
"coverage.reportsDirectory": "{projectRoot}/coverage/unit-test"
}
},
"code-pushup:coverage": {
"cache": true,
"outputs": ["{options.outputPath}"],
"executor": "nx:run-commands",
"options": {
"command": "npx @code-pushup/cli collect",
"config": "{projectRoot}/code-pushup.config.ts",
"cache.write": true,
"persist.skipReports": true,
"persist.outputDir": "{projectRoot}/.code-pushup",
"upload.project": "{projectName}",
"outputPath": "{projectRoot}/.code-pushup/coverage"
},
"dependsOn": ["unit-test", "int-test"]
},
"code-pushup:js-packages": {
"cache": true,
"outputs": ["{options.outputPath}"],
"executor": "nx:run-commands",
"options": {
"command": "npx @code-pushup/cli collect",
"config": "{projectRoot}/code-pushup.config.ts",
"cache.write": true,
"persist.skipReports": true,
"persist.outputDir": "{projectRoot}/.code-pushup",
"upload.project": "{projectName}",
"onlyPlugins": "js-packages",
"outputPath": "{projectRoot}/.code-pushup/js-packages"
}
},
"code-pushup": {
"cache": true,
"outputs": ["{options.outputPath}"],
"executor": "nx:run-commands",
"options": {
"command": "node packages/cli/src/index.ts",
"config": "{projectRoot}/code-pushup.config.ts",
"cache.read": true,
"upload.project": "{projectName}",
"outputPath": "{projectRoot}/.code-pushup"
},
"dependsOn": ["code-pushup:coverage", "code-pushup:js-packages"]
}
}
}
```

## Nx Task Graph

This configuration creates the following task dependency graph:

**Legend:**

- 🐳 = Cached target

```mermaid
graph TD
A[lib-a:code-pushup 🐳] --> B[lib-a:code-pushup:coverage 🐳]
A --> E[lib-a:code-pushup:js-packages]
B --> C[lib-a:unit-test 🐳]
B --> D[lib-a:int-test 🐳]
```

## Command Line Example

```bash
# Run all affected projects e.g. nx run lib-a:code-pushup:coverage
nx affected --target=code-pushup:*

# Run all affected projects and upload the report to the portal
nx run reop-source:code-pushup autorun
```

This approach has the following benefits:

1. **Parallel Execution**: Plugins can run in parallel
2. **Finegrained Caching**: Code level cache invalidation enables usage of [affected](https://nx.dev/recipes/affected-tasks) command
3. **Dependency Management**: Leverage Nx task dependencies and its caching strategy
4. **Clear Separation**: Each plugin has its own target for better debugging and maintainability
104 changes: 104 additions & 0 deletions packages/cli/docs/turbo-caching.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Caching Example Turborepo

To cache plugin runner output with Turborepo, wire Code Pushup into your turbo.json pipeline and pass Code Pushup flags (`--cache.write`, `--cache.read`, `--onlyPlugins`, `--persist.skipReports`) through task scripts. Turborepo will cache task outputs declared in outputs, and you can target affected packages with `--filter=[origin/main]`.

## `{projectRoot}/code-pushup.config.ts`

```ts
import coveragePlugin from '@code-pushup/coverage-plugin';
import jsPackagesPlugin from '@code-pushup/js-packages-plugin';
import type { CoreConfig } from '@code-pushup/models';

export default {
plugins: [
await coveragePlugin({
reports: ['coverage/lcov.info'],
coverageTypes: ['function', 'branch', 'line'],
}),
await jsPackagesPlugin(),
],
upload: {
server: 'https://portal.code-pushup.dev/api',
organization: 'my-org',
project: 'lib-a',
apiKey: process.env.CP_API_KEY,
},
} satisfies CoreConfig;
```

## Root `turbo.json`

```json
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"unit-test": {
"outputs": ["coverage/unit-test/**"]
},
"int-test": {
"outputs": ["coverage/int-test/**"]
},
"code-pushup:coverage": {
"dependsOn": ["unit-test", "int-test"],
"outputs": [".code-pushup/coverage/**"]
},
"code-pushup:js-packages": {
"outputs": [".code-pushup/js-packages/**"]
},
"code-pushup": {
"dependsOn": ["code-pushup:coverage", "code-pushup:js-packages"],
"outputs": [".code-pushup/**"]
}
}
}
```

## `packages/lib-a/package.json`

```json
{
"name": "lib-a",
"scripts": {
"unit-test": "vitest --config packages/lib-a/vitest.unit.config.ts --coverage",
"int-test": "vitest --config packages/lib-a/vitest.int.config.ts --coverage",
"code-pushup:coverage": "code-pushup collect --config packages/lib-a/code-pushup.config.ts --cache.write --persist.skipReports --persist.outputDir packages/lib-a/.code-pushup --onlyPlugins=coverage",
"code-pushup:js-packages": "code-pushup collect --config packages/lib-a/code-pushup.config.ts --cache.write --persist.skipReports --persist.outputDir packages/lib-a/.code-pushup --onlyPlugins=js-packages",
"code-pushup": "code-pushup autorun --config packages/lib-a/code-pushup.config.ts --cache.read --persist.outputDir packages/lib-a/.code-pushup"
}
}
```

> **Note:** `--cache.write` is used on the collect step to persist each plugin's audit-outputs.json; `--cache.read` is used on the autorun step to reuse those outputs.
## Turborepo Task Graph

This configuration creates the following task dependency graph:

**Legend:**

- ⚡ = Cached target (via outputs)

```mermaid
graph TD
A[lib-a:code-pushup ⚡] --> B[lib-a:code-pushup:coverage]
A --> E[lib-a:code-pushup:js-packages]
B --> C[lib-a:unit-test ⚡]
B --> D[lib-a:int-test ⚡]
```

## Command Line Examples

```bash
# Run all affected packages e.g. turbo run code-pushup:coverage --filter=lib-a
turbo run code-pushup:* --filter=[origin/main]

# Run all affected packages and upload the report to the portal
turbo run code-pushup --filter=[origin/main]
```

This approach has the following benefits:

1. **Parallel Execution**: Plugins can run in parallel
2. **Finegrained Caching**: Code level cache invalidation enables usage of affected packages filtering
3. **Dependency Management**: Leverage Turborepo task dependencies and its caching strategy
4. **Clear Separation**: Each plugin has its own target for better debugging and maintainability
15 changes: 15 additions & 0 deletions packages/cli/src/lib/implementation/core-config.middleware.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { autoloadRc, readRcByPath } from '@code-pushup/core';
import {
type CacheConfig,
type CacheConfigObject,
type CoreConfig,
DEFAULT_PERSIST_FILENAME,
DEFAULT_PERSIST_FORMAT,
Expand All @@ -23,6 +25,7 @@ export async function coreConfigMiddleware<
tsconfig,
persist: cliPersist,
upload: cliUpload,
cache: cliCache,
...remainingCliOptions
} = processArgs;
// Search for possible configuration file extensions if path is not given
Expand All @@ -41,8 +44,10 @@ export async function coreConfigMiddleware<
...rcUpload,
...cliUpload,
});

return {
...(config != null && { config }),
cache: normalizeCache(cliCache),
persist: {
outputDir:
cliPersist?.outputDir ??
Expand All @@ -60,5 +65,15 @@ export async function coreConfigMiddleware<
};
}

export const normalizeCache = (cache?: CacheConfig): CacheConfigObject => {
if (cache == null) {
return { write: false, read: false };
}
if (typeof cache === 'boolean') {
return { write: cache, read: cache };
}
return { write: cache.write ?? false, read: cache.read ?? false };
};

export const normalizeFormats = (formats?: string[]): Format[] =>
(formats ?? []).flatMap(format => format.split(',') as Format[]);
14 changes: 13 additions & 1 deletion packages/cli/src/lib/implementation/core-config.model.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import type { CoreConfig, Format, UploadConfig } from '@code-pushup/models';
import type {
CacheConfig,
CoreConfig,
Format,
UploadConfig,
} from '@code-pushup/models';

export type PersistConfigCliOptions = {
'persist.outputDir'?: string;
Expand All @@ -13,6 +18,12 @@ export type UploadConfigCliOptions = {
'upload.server'?: string;
};

export type CacheConfigCliOptions = {
'cache.read'?: boolean;
'cache.write'?: boolean;
cache?: boolean;
};

export type ConfigCliOptions = {
config?: string;
tsconfig?: string;
Expand All @@ -21,4 +32,5 @@ export type ConfigCliOptions = {

export type CoreConfigCliOptions = Pick<CoreConfig, 'persist'> & {
upload?: Partial<Omit<UploadConfig, 'timeout'>>;
cache?: CacheConfig;
};
Loading
Loading