Skip to content

Commit 823ade1

Browse files
committed
feat(ci): add configPatterns as optional performance optimization
1 parent 242435e commit 823ade1

File tree

7 files changed

+653
-335
lines changed

7 files changed

+653
-335
lines changed

packages/ci/README.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,12 @@ Optionally, you can override default options for further customization:
103103
| `nxProjectsFilter` | `string \| string[]` | `'--with-target={task}'` | Arguments passed to [`nx show projects`](https://nx.dev/nx-api/nx/documents/show#projects), only relevant for Nx in [monorepo mode](#monorepo-mode) [^2] |
104104
| `directory` | `string` | `process.cwd()` | Directory in which Code PushUp CLI should run |
105105
| `config` | `string \| null` | `null` [^1] | Path to config file (`--config` option) |
106-
| `silent` | `boolean` | `false` | Hides logs from CLI commands (erros will be printed) |
106+
| `silent` | `boolean` | `false` | Hides logs from CLI commands (errors will be printed) |
107107
| `bin` | `string` | `'npx --no-install code-pushup'` | Command for executing Code PushUp CLI |
108108
| `detectNewIssues` | `boolean` | `true` | Toggles if new issues should be detected and returned in `newIssues` property |
109109
| `logger` | `Logger` | `console` | Logger for reporting progress and encountered problems |
110110
| `skipComment` | `boolean` | `false` | Toggles if comparison comment is posted to PR |
111+
| `configPatterns` | `ConfigPatterns \| null` | `null` | Additional configuration which enables [faster CI runs](#faster-ci-runs-with-configpatterns) |
111112

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

@@ -216,6 +217,32 @@ await runInCI(refs, api, {
216217
});
217218
```
218219

220+
### Faster CI runs with `configPatterns`
221+
222+
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.
223+
224+
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.
225+
226+
```ts
227+
await runInCI(refs, api, {
228+
monorepo: true,
229+
configPatterns: {
230+
persist: {
231+
outputDir: '.code-pushup/{projectName}',
232+
filename: 'report',
233+
format: ['json', 'md'],
234+
},
235+
// optional: will use portal as cache when comparing reports in PRs
236+
upload: {
237+
server: 'https://api.code-pushup.example.com/graphql',
238+
apiKey: 'cp_...',
239+
organization: 'example',
240+
project: '{projectName}',
241+
},
242+
},
243+
});
244+
```
245+
219246
### Monorepo result
220247

221248
In monorepo mode, the resolved object includes the merged diff at the top-level, as well as a list of projects.

packages/ci/src/lib/cli/context.unit.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ describe('createCommandContext', () => {
2424
silent: false,
2525
task: 'code-pushup',
2626
skipComment: false,
27+
configPatterns: null,
2728
},
2829
null,
2930
),
@@ -52,6 +53,7 @@ describe('createCommandContext', () => {
5253
silent: false,
5354
task: 'code-pushup',
5455
skipComment: false,
56+
configPatterns: null,
5557
},
5658
{
5759
name: 'ui',

packages/ci/src/lib/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ export const DEFAULT_SETTINGS: Settings = {
1414
logger: console,
1515
nxProjectsFilter: '--with-target={task}',
1616
skipComment: false,
17+
configPatterns: null,
1718
};

packages/ci/src/lib/models.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Format } from '@code-pushup/models';
1+
import type { Format, PersistConfig, UploadConfig } from '@code-pushup/models';
22
import type { SourceFileIssue } from './issues.js';
33
import type { MonorepoTool } from './monorepo/index.js';
44

@@ -20,6 +20,7 @@ export type Options = {
2020
detectNewIssues?: boolean;
2121
logger?: Logger;
2222
skipComment?: boolean;
23+
configPatterns?: ConfigPatterns | null;
2324
};
2425

2526
/**
@@ -74,6 +75,15 @@ export type Logger = {
7475
debug: (message: string) => void;
7576
};
7677

78+
/**
79+
* Code PushUp config patterns which hold for every project in monorepo.
80+
* Providing this information upfront makes CI runs faster (skips print-config).
81+
*/
82+
export type ConfigPatterns = {
83+
persist: Required<PersistConfig>;
84+
upload?: UploadConfig;
85+
};
86+
7787
/**
7888
* Resolved return value of {@link runInCI}
7989
*/

packages/ci/src/lib/run-monorepo.ts

Lines changed: 66 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable max-lines */
12
import { readFile } from 'node:fs/promises';
23
import {
34
type ExcludeNullableProps,
@@ -32,6 +33,7 @@ import {
3233
type RunEnv,
3334
checkPrintConfig,
3435
compareReports,
36+
configFromPatterns,
3537
hasDefaultPersistFormats,
3638
loadCachedBaseReport,
3739
printPersistConfig,
@@ -85,13 +87,16 @@ export async function runInMonorepoMode(
8587
};
8688
}
8789

88-
type ProjectReport = {
90+
type ProjectEnv = {
8991
project: ProjectConfig;
90-
reports: OutputFiles;
9192
config: EnhancedPersistConfig;
9293
ctx: CommandContext;
9394
};
9495

96+
type ProjectReport = ProjectEnv & {
97+
reports: OutputFiles;
98+
};
99+
95100
function runProjectsIndividually(
96101
projects: ProjectConfig[],
97102
env: RunEnv,
@@ -117,25 +122,12 @@ async function runProjectsInBulk(
117122
`Running on ${projects.length} projects in bulk (parallel: ${settings.parallel})`,
118123
);
119124

120-
const currProjectConfigs = await asyncSequential(projects, async project => {
121-
const ctx = createCommandContext(settings, project);
122-
const config = await printPersistConfig(ctx);
123-
return { project, config, ctx };
124-
});
125-
const hasFormats = allProjectsHaveDefaultPersistFormats(currProjectConfigs);
126-
logger.debug(
127-
[
128-
`Loaded ${currProjectConfigs.length} persist and upload configs by running print-config command for each project.`,
129-
hasFormats
130-
? 'Every project has default persist formats.'
131-
: 'Not all projects have default persist formats.',
132-
].join(' '),
133-
);
125+
const { projectEnvs, hasFormats } = await loadProjectEnvs(projects, settings);
134126

135127
await collectMany(runManyCommand, env, { hasFormats });
136128

137129
const currProjectReports = await Promise.all(
138-
currProjectConfigs.map(
130+
projectEnvs.map(
139131
async ({ project, config, ctx }): Promise<ProjectReport> => {
140132
const reports = await saveOutputFiles({
141133
project,
@@ -155,6 +147,45 @@ async function runProjectsInBulk(
155147
return compareProjectsInBulk(currProjectReports, base, runManyCommand, env);
156148
}
157149

150+
async function loadProjectEnvs(
151+
projects: ProjectConfig[],
152+
settings: Settings,
153+
): Promise<{
154+
projectEnvs: ProjectEnv[];
155+
hasFormats: boolean;
156+
}> {
157+
const { logger, configPatterns } = settings;
158+
159+
const projectEnvs: ProjectEnv[] = configPatterns
160+
? projects.map(
161+
(project): ProjectEnv => ({
162+
project,
163+
config: configFromPatterns(configPatterns, project),
164+
ctx: createCommandContext(settings, project),
165+
}),
166+
)
167+
: await asyncSequential(projects, async (project): Promise<ProjectEnv> => {
168+
const ctx = createCommandContext(settings, project);
169+
const config = await printPersistConfig(ctx);
170+
return { project, config, ctx };
171+
});
172+
173+
const hasFormats = allProjectsHaveDefaultPersistFormats(projectEnvs);
174+
175+
logger.debug(
176+
[
177+
configPatterns
178+
? `Parsed ${projectEnvs.length} persist and upload configs by interpolating configPatterns option for each project.`
179+
: `Loaded ${projectEnvs.length} persist and upload configs by running print-config command for each project.`,
180+
hasFormats
181+
? 'Every project has default persist formats.'
182+
: 'Not all projects have default persist formats.',
183+
].join(' '),
184+
);
185+
186+
return { projectEnvs, hasFormats };
187+
}
188+
158189
async function compareProjectsInBulk(
159190
currProjectReports: ProjectReport[],
160191
base: GitBranch,
@@ -228,27 +259,29 @@ async function collectPreviousReports(
228259
env: RunEnv,
229260
): Promise<Record<string, ReportData<'previous'>>> {
230261
const { settings } = env;
231-
const { logger } = settings;
262+
const { logger, configPatterns } = settings;
232263

233264
if (uncachedProjectReports.length === 0) {
234265
return {};
235266
}
236267

237268
return runInBaseBranch(base, env, async () => {
238-
const uncachedProjectConfigs = await asyncSequential(
239-
uncachedProjectReports,
240-
async args => ({
241-
name: args.project.name,
242-
ctx: args.ctx,
243-
config: await checkPrintConfig(args),
244-
}),
245-
);
269+
const uncachedProjectConfigs = configPatterns
270+
? uncachedProjectReports.map(({ project, ctx }) => {
271+
const config = configFromPatterns(configPatterns, project);
272+
return { project, ctx, config };
273+
})
274+
: await asyncSequential(uncachedProjectReports, async args => ({
275+
project: args.project,
276+
ctx: args.ctx,
277+
config: await checkPrintConfig(args),
278+
}));
246279

247280
const validProjectConfigs =
248281
uncachedProjectConfigs.filter(hasNoNullableProps);
249-
const onlyProjects = validProjectConfigs.map(({ name }) => name);
250-
const invalidProjects = uncachedProjectConfigs
251-
.map(({ name }) => name)
282+
const onlyProjects = validProjectConfigs.map(({ project }) => project.name);
283+
const invalidProjects: string[] = uncachedProjectConfigs
284+
.map(({ project }) => project.name)
252285
.filter(name => !onlyProjects.includes(name));
253286
if (invalidProjects.length > 0) {
254287
logger.debug(
@@ -277,19 +310,19 @@ async function collectPreviousReports(
277310
}
278311

279312
async function savePreviousProjectReport(args: {
280-
name: string;
313+
project: ProjectConfig;
281314
ctx: CommandContext;
282315
config: EnhancedPersistConfig;
283316
settings: Settings;
284317
}): Promise<[string, ReportData<'previous'>]> {
285-
const { name, ctx, config, settings } = args;
318+
const { project, ctx, config, settings } = args;
286319
const files = await saveReportFiles({
287-
project: { name },
320+
project,
288321
type: 'previous',
289322
files: persistedFilesFromConfig(config, ctx),
290323
settings,
291324
});
292-
return [name, files];
325+
return [project.name, files];
293326
}
294327

295328
async function collectMany(

packages/ci/src/lib/run-utils.ts

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
type ReportsDiff,
88
} from '@code-pushup/models';
99
import {
10+
interpolate,
1011
removeUndefinedAndEmptyProps,
1112
stringifyError,
1213
} from '@code-pushup/utils';
@@ -24,6 +25,7 @@ import { DEFAULT_SETTINGS } from './constants.js';
2425
import { listChangedFiles, normalizeGitRef } from './git.js';
2526
import { type SourceFileIssue, filterRelevantIssues } from './issues.js';
2627
import type {
28+
ConfigPatterns,
2729
GitBranch,
2830
GitRefs,
2931
Options,
@@ -76,9 +78,8 @@ export async function createRunEnv(
7678
options: Options | undefined,
7779
git: SimpleGit,
7880
): Promise<RunEnv> {
79-
const inferredVerbose: boolean = Boolean(
80-
options?.debug === true || options?.silent === false,
81-
);
81+
const inferredVerbose: boolean =
82+
options?.debug === true || options?.silent === false;
8283
// eslint-disable-next-line functional/immutable-data
8384
process.env['CP_VERBOSE'] = `${inferredVerbose}`;
8485

@@ -114,9 +115,13 @@ export async function runOnProject(
114115
logger.info(`Running Code PushUp on monorepo project ${project.name}`);
115116
}
116117

117-
const config = await printPersistConfig(ctx);
118+
const config = settings.configPatterns
119+
? configFromPatterns(settings.configPatterns, project)
120+
: await printPersistConfig(ctx);
118121
logger.debug(
119-
`Loaded persist and upload configs from print-config command - ${JSON.stringify(config)}`,
122+
settings.configPatterns
123+
? `Parsed persist and upload configs from configPatterns option - ${JSON.stringify(config)}`
124+
: `Loaded persist and upload configs from print-config command - ${JSON.stringify(config)}`,
120125
);
121126

122127
await runCollect(ctx, { hasFormats: hasDefaultPersistFormats(config) });
@@ -216,15 +221,17 @@ export async function collectPreviousReport(
216221
): Promise<ReportData<'previous'> | null> {
217222
const { ctx, env, base, project } = args;
218223
const { settings } = env;
219-
const { logger } = settings;
224+
const { logger, configPatterns } = settings;
220225

221226
const cachedBaseReport = await loadCachedBaseReport(args);
222227
if (cachedBaseReport) {
223228
return cachedBaseReport;
224229
}
225230

226231
return runInBaseBranch(base, env, async () => {
227-
const config = await checkPrintConfig(args);
232+
const config = configPatterns
233+
? configFromPatterns(configPatterns, project)
234+
: await checkPrintConfig(args);
228235
if (!config) {
229236
return null;
230237
}
@@ -399,6 +406,32 @@ export function hasDefaultPersistFormats(
399406
);
400407
}
401408

409+
export function configFromPatterns(
410+
configPatterns: ConfigPatterns,
411+
project: ProjectConfig | null,
412+
): ConfigPatterns {
413+
const { persist, upload } = configPatterns;
414+
const variables = {
415+
projectName: project?.name ?? '',
416+
};
417+
return {
418+
persist: {
419+
outputDir: interpolate(persist.outputDir, variables),
420+
filename: interpolate(persist.filename, variables),
421+
format: persist.format,
422+
},
423+
...(upload && {
424+
upload: {
425+
server: upload.server,
426+
apiKey: upload.apiKey,
427+
organization: interpolate(upload.organization, variables),
428+
project: interpolate(upload.project, variables),
429+
...(upload.timeout != null && { timeout: upload.timeout }),
430+
},
431+
}),
432+
};
433+
}
434+
402435
export async function findNewIssues(
403436
args: CompareReportsArgs & { diffFiles: OutputFiles },
404437
): Promise<SourceFileIssue[]> {

0 commit comments

Comments
 (0)