Skip to content

Commit b0a7abc

Browse files
committed
refactor(ci): split up overlarge run.ts and runProjectsInBulk
1 parent 2404074 commit b0a7abc

File tree

6 files changed

+638
-574
lines changed

6 files changed

+638
-574
lines changed
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
export { listMonorepoProjects } from './list-projects.js';
1+
export { listMonorepoProjects, type RunManyCommand } from './list-projects.js';
22
export {
3-
MONOREPO_TOOLS,
43
isMonorepoTool,
4+
MONOREPO_TOOLS,
55
type MonorepoTool,
66
type ProjectConfig,
77
} from './tools.js';

packages/ci/src/lib/monorepo/list-projects.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,13 @@ import type {
1313
export type MonorepoProjects = {
1414
tool: MonorepoTool | null;
1515
projects: ProjectConfig[];
16-
runManyCommand?: (onlyProjects?: string[]) => string | Promise<string>;
16+
runManyCommand?: RunManyCommand;
1717
};
1818

19+
export type RunManyCommand = (
20+
onlyProjects?: string[],
21+
) => string | Promise<string>;
22+
1923
export async function listMonorepoProjects(
2024
settings: Settings,
2125
): Promise<MonorepoProjects> {
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
import { copyFile, readFile } from 'node:fs/promises';
2+
import { basename, join } from 'node:path';
3+
import {
4+
type CoreConfig,
5+
DEFAULT_PERSIST_OUTPUT_DIR,
6+
} from '@code-pushup/models';
7+
import {
8+
type ExcludeNullableProps,
9+
hasNoNullableProps,
10+
} from '@code-pushup/utils';
11+
import {
12+
type CommandContext,
13+
createCommandContext,
14+
persistedFilesFromConfig,
15+
runCollect,
16+
runMergeDiffs,
17+
} from './cli/index.js';
18+
import { commentOnPR } from './comment.js';
19+
import type {
20+
GitBranch,
21+
MonorepoRunResult,
22+
OutputFiles,
23+
ProjectRunResult,
24+
} from './models.js';
25+
import {
26+
type ProjectConfig,
27+
type RunManyCommand,
28+
listMonorepoProjects,
29+
} from './monorepo/index.js';
30+
import {
31+
type BaseReportArgs,
32+
type CompareReportsResult,
33+
type RunEnv,
34+
checkPrintConfig,
35+
compareReports,
36+
loadCachedBaseReport,
37+
printPersistConfig,
38+
runInBaseBranch,
39+
runOnProject,
40+
} from './run-utils.js';
41+
42+
export async function runInMonorepoMode(
43+
env: RunEnv,
44+
): Promise<MonorepoRunResult> {
45+
const { api, settings } = env;
46+
const { logger, directory } = settings;
47+
48+
logger.info('Running Code PushUp in monorepo mode');
49+
50+
const { projects, runManyCommand } = await listMonorepoProjects(settings);
51+
const projectResults = runManyCommand
52+
? await runProjectsInBulk(projects, runManyCommand, env)
53+
: await runProjectsIndividually(projects, env);
54+
55+
const diffJsonPaths = projectResults
56+
.map(({ files }) => files.diff?.json)
57+
.filter((file): file is string => file != null);
58+
if (diffJsonPaths.length > 0) {
59+
const tmpDiffPath = await runMergeDiffs(
60+
diffJsonPaths,
61+
createCommandContext(settings, projects[0]),
62+
);
63+
logger.debug(`Merged ${diffJsonPaths.length} diffs into ${tmpDiffPath}`);
64+
const diffPath = join(
65+
directory,
66+
DEFAULT_PERSIST_OUTPUT_DIR,
67+
basename(tmpDiffPath),
68+
);
69+
if (tmpDiffPath !== diffPath) {
70+
await copyFile(tmpDiffPath, diffPath);
71+
logger.debug(`Copied ${tmpDiffPath} to ${diffPath}`);
72+
}
73+
const commentId = await commentOnPR(tmpDiffPath, api, logger);
74+
return {
75+
mode: 'monorepo',
76+
projects: projectResults,
77+
commentId,
78+
diffPath,
79+
};
80+
}
81+
82+
return { mode: 'monorepo', projects: projectResults };
83+
}
84+
85+
type ProjectReport = {
86+
project: ProjectConfig;
87+
reports: OutputFiles;
88+
config: Pick<CoreConfig, 'persist'>;
89+
ctx: CommandContext;
90+
};
91+
92+
function runProjectsIndividually(
93+
projects: ProjectConfig[],
94+
env: RunEnv,
95+
): Promise<ProjectRunResult[]> {
96+
env.settings.logger.info(
97+
`Running on ${projects.length} projects individually`,
98+
);
99+
return projects.reduce<Promise<ProjectRunResult[]>>(
100+
async (acc, project) => [...(await acc), await runOnProject(project, env)],
101+
Promise.resolve([]),
102+
);
103+
}
104+
105+
async function runProjectsInBulk(
106+
projects: ProjectConfig[],
107+
runManyCommand: RunManyCommand,
108+
env: RunEnv,
109+
): Promise<ProjectRunResult[]> {
110+
const {
111+
refs: { base },
112+
settings,
113+
} = env;
114+
const logger = settings.logger;
115+
116+
logger.info(
117+
`Running on ${projects.length} projects in bulk (parallel: ${settings.parallel})`,
118+
);
119+
120+
await collectMany(runManyCommand, env);
121+
122+
const currProjectReports = await Promise.all(
123+
projects.map(async (project): Promise<ProjectReport> => {
124+
const ctx = createCommandContext(settings, project);
125+
const config = await printPersistConfig(ctx, settings);
126+
const reports = persistedFilesFromConfig(config, ctx);
127+
return { project, reports, config, ctx };
128+
}),
129+
);
130+
logger.debug(
131+
`Loaded ${currProjectReports.length} persist configs by running print-config command for each project`,
132+
);
133+
134+
if (base == null) {
135+
return finalizeProjectReports(currProjectReports);
136+
}
137+
138+
return compareProjectsInBulk(currProjectReports, base, runManyCommand, env);
139+
}
140+
141+
async function compareProjectsInBulk(
142+
currProjectReports: ProjectReport[],
143+
base: GitBranch,
144+
runManyCommand: RunManyCommand,
145+
env: RunEnv,
146+
): Promise<ProjectRunResult[]> {
147+
const projectReportsWithCache = await Promise.all(
148+
currProjectReports.map(async ({ project, ctx, reports, config }) => {
149+
const args = { project, base, ctx, env };
150+
return {
151+
...args,
152+
config,
153+
currReport: await readFile(reports.json, 'utf8'),
154+
prevReport: await loadCachedBaseReport(args),
155+
};
156+
}),
157+
);
158+
const uncachedProjectReports = projectReportsWithCache.filter(
159+
({ prevReport }) => !prevReport,
160+
);
161+
env.settings.logger.info(
162+
`${currProjectReports.length - uncachedProjectReports.length} out of ${currProjectReports.length} projects loaded previous report from artifact cache`,
163+
);
164+
165+
const collectedPrevReports = await collectPreviousReports(
166+
base,
167+
uncachedProjectReports,
168+
runManyCommand,
169+
env,
170+
);
171+
172+
const projectsToCompare = projectReportsWithCache
173+
.map(args => ({
174+
...args,
175+
prevReport: args.prevReport || collectedPrevReports[args.project.name],
176+
}))
177+
.filter(hasNoNullableProps);
178+
179+
const projectComparisons = await projectsToCompare.reduce<
180+
Promise<Record<string, CompareReportsResult>>
181+
>(
182+
async (acc, args) => ({
183+
...(await acc),
184+
[args.project.name]: await compareReports(args),
185+
}),
186+
Promise.resolve({}),
187+
);
188+
189+
return finalizeProjectReports(currProjectReports, projectComparisons);
190+
}
191+
192+
function finalizeProjectReports(
193+
projectReports: ProjectReport[],
194+
projectComparisons?: Record<string, CompareReportsResult>,
195+
): ProjectRunResult[] {
196+
return projectReports.map(({ project, reports }): ProjectRunResult => {
197+
const comparison = projectComparisons?.[project.name];
198+
return {
199+
name: project.name,
200+
files: {
201+
report: reports,
202+
...(comparison && { diff: comparison.files }),
203+
},
204+
...(comparison?.newIssues && { newIssues: comparison.newIssues }),
205+
};
206+
});
207+
}
208+
209+
async function collectPreviousReports(
210+
base: GitBranch,
211+
uncachedProjectReports: ExcludeNullableProps<BaseReportArgs>[],
212+
runManyCommand: RunManyCommand,
213+
env: RunEnv,
214+
): Promise<Record<string, string>> {
215+
const {
216+
settings: { logger },
217+
} = env;
218+
219+
if (uncachedProjectReports.length === 0) {
220+
return {};
221+
}
222+
223+
return runInBaseBranch(base, env, async () => {
224+
const uncachedProjectConfigs = await Promise.all(
225+
uncachedProjectReports.map(async args => ({
226+
name: args.project.name,
227+
ctx: args.ctx,
228+
config: await checkPrintConfig(args),
229+
})),
230+
);
231+
232+
const validProjectConfigs =
233+
uncachedProjectConfigs.filter(hasNoNullableProps);
234+
const onlyProjects = validProjectConfigs.map(({ name }) => name);
235+
const invalidProjects = uncachedProjectConfigs
236+
.map(({ name }) => name)
237+
.filter(name => !onlyProjects.includes(name));
238+
if (invalidProjects.length > 0) {
239+
logger.debug(
240+
`Printing config failed for ${invalidProjects.length} projects - ${invalidProjects.join(', ')}`,
241+
);
242+
logger.info(
243+
`Skipping ${invalidProjects.length} projects which aren't configured in base branch ${base.ref}`,
244+
);
245+
}
246+
247+
if (onlyProjects.length > 0) {
248+
logger.info(
249+
`Collecting previous reports for ${onlyProjects.length} projects`,
250+
);
251+
await collectMany(runManyCommand, env, onlyProjects);
252+
}
253+
254+
const projectFiles = validProjectConfigs.map(
255+
async ({ name, ctx, config }) =>
256+
[
257+
name,
258+
await readFile(persistedFilesFromConfig(config, ctx).json, 'utf8'),
259+
] as const,
260+
);
261+
262+
return Object.fromEntries(await Promise.all(projectFiles));
263+
});
264+
}
265+
266+
async function collectMany(
267+
runManyCommand: RunManyCommand,
268+
env: RunEnv,
269+
onlyProjects?: string[],
270+
): Promise<void> {
271+
const { settings } = env;
272+
const command = await runManyCommand(onlyProjects);
273+
const ctx: CommandContext = {
274+
...createCommandContext(settings, null),
275+
bin: command,
276+
};
277+
278+
await runCollect(ctx);
279+
280+
const countText = onlyProjects
281+
? `${onlyProjects.length} previous`
282+
: 'all current';
283+
settings.logger.debug(
284+
`Collected ${countText} reports using command \`${command}\``,
285+
);
286+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { commentOnPR } from './comment.js';
2+
import type { StandaloneRunResult } from './models.js';
3+
import { type RunEnv, runOnProject } from './run-utils.js';
4+
5+
export async function runInStandaloneMode(
6+
env: RunEnv,
7+
): Promise<StandaloneRunResult> {
8+
const {
9+
api,
10+
settings: { logger },
11+
} = env;
12+
13+
logger.info('Running Code PushUp in standalone project mode');
14+
15+
const { files, newIssues } = await runOnProject(null, env);
16+
17+
const commentMdPath = files.diff?.md;
18+
if (commentMdPath) {
19+
const commentId = await commentOnPR(commentMdPath, api, logger);
20+
return {
21+
mode: 'standalone',
22+
files,
23+
commentId,
24+
newIssues,
25+
};
26+
}
27+
return { mode: 'standalone', files, newIssues };
28+
}

0 commit comments

Comments
 (0)