Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"node": ">=22.14"
},
"dependencies": {
"@code-pushup/portal-client": "^0.14.3",
"@code-pushup/portal-client": "^0.15.0",
"@isaacs/cliui": "^8.0.2",
"@nx/devkit": "19.8.13",
"@poppinss/cliui": "6.4.1",
Expand Down
72 changes: 46 additions & 26 deletions packages/ci/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ Optionally, you can override default options for further customization:
| `logger` | `Logger` | `console` | Logger for reporting progress and encountered problems |
| `skipComment` | `boolean` | `false` | Toggles if comparison comment is posted to PR |
| `configPatterns` | `ConfigPatterns \| null` | `null` | Additional configuration which enables [faster CI runs](#faster-ci-runs-with-configpatterns) |
| `searchCommits` | `boolean \| number` | `false` | If base branch has no cached report in portal, [extends search up to 100 recent commits](#search-latest-commits-for-previous-report) |

[^1]: By default, the `code-pushup.config` file is autodetected as described in [`@code-pushup/cli` docs](../cli/README.md#configuration).

Expand Down Expand Up @@ -217,32 +218,6 @@ await runInCI(refs, api, {
});
```

### Faster CI runs with `configPatterns`

By default, the `print-config` command is run sequentially for each project in order to reliably detect how `code-pushup` is configured - specifically, where to read output files from (`persist` config) and whether portal may be used as a cache (`upload` config). This allows for each project to be configured in its own way without breaking anything, but for large monorepos these extra `code-pushup print-config` executions can accumulate and significantly slow down CI pipelines.

As a more scalable alternative, `configPatterns` may be provided. A user declares upfront how every project is configured, which allows `print-config` to be skipped. It's the user's responsibility to ensure this configuration holds for every project (it won't be checked). The `configPatterns` support string interpolation, substituting `{projectName}` with each project's name. Other than that, each project's `code-pushup.config` must have exactly the same `persist` and `upload` configurations.

```ts
await runInCI(refs, api, {
monorepo: true,
configPatterns: {
persist: {
outputDir: '.code-pushup/{projectName}',
filename: 'report',
format: ['json', 'md'],
},
// optional: will use portal as cache when comparing reports in PRs
upload: {
server: 'https://api.code-pushup.example.com/graphql',
apiKey: 'cp_...',
organization: 'example',
project: '{projectName}',
},
},
});
```

### Monorepo result

In monorepo mode, the resolved object includes the merged diff at the top-level, as well as a list of projects.
Expand Down Expand Up @@ -273,3 +248,48 @@ if (result.mode === 'monorepo') {
}
}
```

## Advanced usage

### Faster CI runs with `configPatterns`

By default, the `print-config` command is run sequentially for each project in order to reliably detect how `code-pushup` is configured - specifically, where to read output files from (`persist` config) and whether portal may be used as a cache (`upload` config). This allows for each project to be configured in its own way without breaking anything, but for large monorepos these extra `code-pushup print-config` executions can accumulate and significantly slow down CI pipelines.

As a more scalable alternative, `configPatterns` may be provided. A user declares upfront how every project is configured, which allows `print-config` to be skipped. It's the user's responsibility to ensure this configuration holds for every project (it won't be checked). The `configPatterns` support string interpolation, substituting `{projectName}` with each project's name. Other than that, each project's `code-pushup.config` must have exactly the same `persist` and `upload` configurations.

```ts
await runInCI(refs, api, {
monorepo: true,
configPatterns: {
persist: {
outputDir: '.code-pushup/{projectName}',
filename: 'report',
format: ['json', 'md'],
},
// optional: will use portal as cache when comparing reports in PRs
upload: {
server: 'https://api.code-pushup.example.com/graphql',
apiKey: 'cp_...',
organization: 'example',
project: '{projectName}',
},
},
});
```

### Search latest commits for previous report

When comparing reports, the report for the base branch can be cached. If a project has an `upload` configuration, then the Portal API is queried for a report matching that commit. If no such report was uploaded, then the report is looked up in CI artifacts (implemented in `downloadReportArtifact` in [`ProviderApiClient`](#provider-api-client)). If there's no report to be found, then the base branch is checked and the previous report is collected.

In some scenarios, there may not be a report for the latest commit in main branch, but some other recent commit may have a usable report - e.g. if `nxProjectsFilter` is used with `--affected` flag. In that case, the `searchCommits` option can be enabled. Then a limited number of recent commits in the main branch will be checked, but.

```ts
await runInCI(refs, api, {
monorepo: 'nx',
nxProjectsFilter: '--with-target=code-pushup --affected',
// checks 10 most recent commits by default
searchCommits: true,
// optionally, number of searched commits may be extended up to 100
// searchCommits: 30
});
```
2 changes: 1 addition & 1 deletion packages/ci/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"type": "module",
"dependencies": {
"@code-pushup/models": "0.72.1",
"@code-pushup/portal-client": "^0.14.3",
"@code-pushup/portal-client": "^0.15.0",
"@code-pushup/utils": "0.72.1",
"glob": "^11.0.1",
"simple-git": "^3.20.0",
Expand Down
4 changes: 4 additions & 0 deletions packages/ci/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,8 @@ export const DEFAULT_SETTINGS: Settings = {
nxProjectsFilter: '--with-target={task}',
skipComment: false,
configPatterns: null,
searchCommits: false,
};

export const MIN_SEARCH_COMMITS = 1;
export const MAX_SEARCH_COMMITS = 100;
1 change: 1 addition & 0 deletions packages/ci/src/lib/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type Options = {
logger?: Logger;
skipComment?: boolean;
configPatterns?: ConfigPatterns | null;
searchCommits?: boolean | number;
};

/**
Expand Down
10 changes: 5 additions & 5 deletions packages/ci/src/lib/portal/download.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { mkdir, writeFile } from 'node:fs/promises';
import path from 'node:path';
import {
type PortalDownloadArgs,
downloadFromPortal,
type PortalReportDownloadArgs,
downloadReportFromPortal,
} from '@code-pushup/portal-client';
import { transformGQLReport } from './transform.js';

export async function downloadReportFromPortal(
args: PortalDownloadArgs,
export async function downloadFromPortal(
args: PortalReportDownloadArgs,
): Promise<string | null> {
const gqlReport = await downloadFromPortal(args);
const gqlReport = await downloadReportFromPortal(args);
if (!gqlReport) {
return null;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/ci/src/lib/portal/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { downloadReportFromPortal } from './download.js';
export { downloadFromPortal } from './download.js';
50 changes: 44 additions & 6 deletions packages/ci/src/lib/run-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,18 @@ import {
runCompare,
runPrintConfig,
} from './cli/index.js';
import { DEFAULT_SETTINGS } from './constants.js';
import {
DEFAULT_SETTINGS,
MAX_SEARCH_COMMITS,
MIN_SEARCH_COMMITS,
} from './constants.js';
import { listChangedFiles, normalizeGitRef } from './git.js';
import { type SourceFileIssue, filterRelevantIssues } from './issues.js';
import type {
ConfigPatterns,
GitBranch,
GitRefs,
Logger,
Options,
OutputFiles,
ProjectRunResult,
Expand All @@ -43,7 +48,7 @@ import type {
} from './models.js';
import type { ProjectConfig } from './monorepo/index.js';
import { saveOutputFiles } from './output-files.js';
import { downloadReportFromPortal } from './portal/download.js';
import { downloadFromPortal } from './portal/download.js';

export type RunEnv = {
refs: NormalizedGitRefs;
Expand Down Expand Up @@ -101,12 +106,40 @@ export async function createRunEnv(
api,
settings: {
...DEFAULT_SETTINGS,
...(options && removeUndefinedAndEmptyProps(options)),
...(options && sanitizeOptions(options)),
},
git,
};
}

function sanitizeOptions(options: Options): Options {
const logger = options.logger ?? DEFAULT_SETTINGS.logger;

return removeUndefinedAndEmptyProps({
...options,
searchCommits: sanitizeSearchCommits(options.searchCommits, logger),
});
}

function sanitizeSearchCommits(
searchCommits: Options['searchCommits'],
logger: Logger,
): Options['searchCommits'] {
if (
typeof searchCommits === 'number' &&
(!Number.isInteger(searchCommits) ||
searchCommits < MIN_SEARCH_COMMITS ||
searchCommits > MAX_SEARCH_COMMITS)
) {
logger.warn(
`The searchCommits option must be a boolean or an integer in range ${MIN_SEARCH_COMMITS} to ${MAX_SEARCH_COMMITS}, ignoring invalid value ${searchCommits}.`,
);
return undefined;
}

return searchCommits;
}

export async function runOnProject(
project: ProjectConfig | null,
env: RunEnv,
Expand Down Expand Up @@ -376,14 +409,19 @@ async function loadCachedBaseReportFromPortal(
return null;
}

const reportPath = await downloadReportFromPortal({
const reportPath = await downloadFromPortal({
server: config.upload.server,
apiKey: config.upload.apiKey,
parameters: {
organization: config.upload.organization,
project: config.upload.project,
commit: base.sha,
withDetails: true,
withAuditDetails: true,
...(!settings.searchCommits && {
commit: base.sha,
}),
...(typeof settings.searchCommits === 'number' && {
maxCommits: settings.searchCommits,
}),
},
}).catch((error: unknown) => {
logger.warn(
Expand Down
12 changes: 6 additions & 6 deletions packages/ci/src/lib/run.int.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { type SimpleGit, simpleGit } from 'simple-git';
import { type MockInstance, expect } from 'vitest';
import {
type ReportFragment,
downloadFromPortal,
downloadReportFromPortal,
} from '@code-pushup/portal-client';
import {
type CoreConfig,
Expand Down Expand Up @@ -43,7 +43,7 @@ vi.mock('@code-pushup/portal-client', async importOriginal => {
await importOriginal();
return {
...mod,
downloadFromPortal: vi.fn(simulateDownloadFromPortal),
downloadReportFromPortal: vi.fn(simulateDownloadReportFromPortal),
};
});

Expand Down Expand Up @@ -84,7 +84,7 @@ const fixturePaths = {
},
};

function simulateDownloadFromPortal() {
function simulateDownloadReportFromPortal() {
return utils.readJsonFile<ReportFragment>(fixturePaths.reports.before.portal);
}

Expand Down Expand Up @@ -500,16 +500,16 @@ describe('runInCI', () => {
},
} satisfies RunResult);

expect(downloadFromPortal).toHaveBeenCalledWith<
Parameters<typeof downloadFromPortal>
expect(downloadReportFromPortal).toHaveBeenCalledWith<
Parameters<typeof downloadReportFromPortal>
>({
server: 'https://api.code-pushup.dunder-mifflin.org/graphql',
apiKey: 'cp_abcdef0123456789',
parameters: {
organization: 'dunder-mifflin',
project: 'website',
commit: refs.base.sha,
withDetails: true,
withAuditDetails: true,
},
});

Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/lib/autorun/autorun-command.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { vol } from 'memfs';
import { describe, expect, it, vi } from 'vitest';
import { uploadToPortal } from '@code-pushup/portal-client';
import { uploadReportToPortal } from '@code-pushup/portal-client';
import { collectAndPersistReports, readRcByPath } from '@code-pushup/core';
import { MEMFS_VOLUME, MINIMAL_REPORT_MOCK } from '@code-pushup/test-utils';
import { DEFAULT_CLI_CONFIGURATION } from '../../../mocks/constants.js';
Expand Down Expand Up @@ -58,8 +58,8 @@ describe('autorun-command', () => {
);

// values come from CORE_CONFIG_MOCK returned by readRcByPath mock
expect(uploadToPortal).toHaveBeenCalledWith<
Parameters<typeof uploadToPortal>
expect(uploadReportToPortal).toHaveBeenCalledWith<
Parameters<typeof uploadReportToPortal>
>({
apiKey: 'dummy-api-key',
server: 'https://example.com/api',
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/lib/upload/upload-command.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { vol } from 'memfs';
import { describe, expect, it } from 'vitest';
import { uploadToPortal } from '@code-pushup/portal-client';
import { uploadReportToPortal } from '@code-pushup/portal-client';
import { readRcByPath } from '@code-pushup/core';
import {
ISO_STRING_REGEXP,
Expand Down Expand Up @@ -52,8 +52,8 @@ describe('upload-command-object', () => {
);

// values come from CORE_CONFIG_MOCK returned by readRcByPath mock
expect(uploadToPortal).toHaveBeenCalledWith<
Parameters<typeof uploadToPortal>
expect(uploadReportToPortal).toHaveBeenCalledWith<
Parameters<typeof uploadReportToPortal>
>({
apiKey: 'dummy-api-key',
server: 'https://example.com/api',
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"ansis": "^3.3.0"
},
"peerDependencies": {
"@code-pushup/portal-client": "^0.14.3"
"@code-pushup/portal-client": "^0.15.0"
},
"peerDependenciesMeta": {
"@code-pushup/portal-client": {
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/lib/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export async function upload(options: UploadOptions) {
if (!portalClient) {
return;
}
const { uploadToPortal } = portalClient;
const { uploadReportToPortal } = portalClient;
const { apiKey, server, organization, project, timeout } = options.upload;
const report: Report = await loadReport({
...options.persist,
Expand All @@ -39,5 +39,5 @@ export async function upload(options: UploadOptions) {
...reportToGQL(report),
};

return uploadToPortal({ apiKey, server, data, timeout });
return uploadReportToPortal({ apiKey, server, data, timeout });
}
Loading
Loading