Skip to content

Commit

Permalink
feat(ci): implement run many command resolution for each monorepo tool
Browse files Browse the repository at this point in the history
  • Loading branch information
matejchalk committed Dec 16, 2024
1 parent 0b9d679 commit 094797d
Show file tree
Hide file tree
Showing 9 changed files with 286 additions and 104 deletions.
11 changes: 9 additions & 2 deletions packages/ci/src/lib/monorepo/handlers/npm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ import type { MonorepoToolHandler } from '../tools.js';

export const npmHandler: MonorepoToolHandler = {
tool: 'npm',

async isConfigured(options) {
return (
(await fileExists(join(options.cwd, 'package-lock.json'))) &&
(await hasWorkspacesEnabled(options.cwd))
);
},

async listProjects(options) {
const { workspaces, rootPackageJson } = await listWorkspaces(options.cwd);
return workspaces
Expand All @@ -28,8 +30,13 @@ export const npmHandler: MonorepoToolHandler = {
.map(({ name, packageJson }) => ({
name,
bin: hasScript(packageJson, options.task)
? `npm -w ${name} run ${options.task} --`
: `npm -w ${name} exec ${options.task} --`,
? `npm --workspace=${name} run ${options.task} --`
: `npm --workspace=${name} exec ${options.task} --`,
}));
},

createRunManyCommand(options) {
// neither parallel execution nor projects filter are supported in NPM workspaces
return `npm run ${options.task} --workspaces --if-present --`;
},
};
16 changes: 16 additions & 0 deletions packages/ci/src/lib/monorepo/handlers/nx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { MonorepoToolHandler } from '../tools.js';

export const nxHandler: MonorepoToolHandler = {
tool: 'nx',

async isConfigured(options) {
return (
(await fileExists(join(options.cwd, 'nx.json'))) &&
Expand All @@ -18,10 +19,12 @@ export const nxHandler: MonorepoToolHandler = {
args: ['nx', 'report'],
cwd: options.cwd,
observer: options.observer,
ignoreExitCode: true,
})
).code === 0
);
},

async listProjects(options) {
const { stdout } = await executeProcess({
command: 'npx',
Expand All @@ -43,6 +46,19 @@ export const nxHandler: MonorepoToolHandler = {
bin: `npx nx run ${project}:${options.task} --`,
}));
},

createRunManyCommand(options, onlyProjects) {
return [
'npx',
'nx',
'run-many', // TODO: allow affected instead of run-many?
`--targets=${options.task}`,
// TODO: add options.nxRunManyFilter? (e.g. --exclude=...)
...(onlyProjects ? [`--projects=${onlyProjects.join(',')}`] : []),
`--parallel=${options.parallel}`,
'--',
].join(' ');
},
};

function parseProjects(stdout: string): string[] {
Expand Down
26 changes: 24 additions & 2 deletions packages/ci/src/lib/monorepo/handlers/pnpm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,19 @@ import type { MonorepoToolHandler } from '../tools.js';

const WORKSPACE_FILE = 'pnpm-workspace.yaml';

// https://pnpm.io/cli/recursive#--workspace-concurrency
const DEFAULT_WORKSPACE_CONCURRENCY = 4;

export const pnpmHandler: MonorepoToolHandler = {
tool: 'pnpm',

async isConfigured(options) {
return (
(await fileExists(join(options.cwd, WORKSPACE_FILE))) &&
(await fileExists(join(options.cwd, 'package.json')))
);
},

async listProjects(options) {
const yaml = await readTextFile(join(options.cwd, WORKSPACE_FILE));
const workspace = YAML.parse(yaml) as { packages?: string[] };
Expand All @@ -34,8 +39,25 @@ export const pnpmHandler: MonorepoToolHandler = {
.map(({ name, packageJson }) => ({
name,
bin: hasScript(packageJson, options.task)
? `pnpm -F ${name} run ${options.task}`
: `pnpm -F ${name} exec ${options.task}`,
? `pnpm --filter=${name} run ${options.task}`
: `pnpm --filter=${name} exec ${options.task}`,
}));
},

createRunManyCommand(options, onlyProjects) {
const workspaceConcurrency: number =
options.parallel === true
? DEFAULT_WORKSPACE_CONCURRENCY
: options.parallel === false
? 1
: options.parallel;
return [
'pnpm',
'--recursive',
`--workspace-concurrency=${workspaceConcurrency}`,
...(onlyProjects?.map(project => `--filter=${project}`) ?? []),
'run',
options.task,
].join(' ');
},
};
25 changes: 24 additions & 1 deletion packages/ci/src/lib/monorepo/handlers/turbo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,24 @@ import { yarnHandler } from './yarn.js';

const WORKSPACE_HANDLERS = [pnpmHandler, yarnHandler, npmHandler];

// https://turbo.build/repo/docs/reference/run#--concurrency-number--percentage
const DEFAULT_CONCURRENCY = 10;

type TurboConfig = {
tasks: Record<string, object>;
};

export const turboHandler: MonorepoToolHandler = {
tool: 'turbo',

async isConfigured(options) {
const configPath = join(options.cwd, 'turbo.json');
return (
(await fileExists(configPath)) &&
options.task in (await readJsonFile<TurboConfig>(configPath)).tasks
);
},

async listProjects(options) {
// eslint-disable-next-line functional/no-loop-statements
for (const handler of WORKSPACE_HANDLERS) {
Expand All @@ -29,7 +34,7 @@ export const turboHandler: MonorepoToolHandler = {
.filter(({ bin }) => bin.includes(`run ${options.task}`)) // must have package.json script
.map(({ name }) => ({
name,
bin: `npx turbo run ${options.task} -F ${name} --`,
bin: `npx turbo run ${options.task} --filter=${name} --`,
}));
}
}
Expand All @@ -39,4 +44,22 @@ export const turboHandler: MonorepoToolHandler = {
).join('/')}`,
);
},

createRunManyCommand(options, onlyProjects) {
const concurrency: number =
options.parallel === true
? DEFAULT_CONCURRENCY
: options.parallel === false
? 1
: options.parallel;
return [
'npx',
'turbo',
'run',
options.task,
...(onlyProjects?.map(project => `--filter=${project}`) ?? []),
`--concurrency=${concurrency}`,
'--',
].join(' ');
},
};
26 changes: 25 additions & 1 deletion packages/ci/src/lib/monorepo/handlers/yarn.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { join } from 'node:path';
import { fileExists } from '@code-pushup/utils';
import { executeProcess, fileExists } from '@code-pushup/utils';
import {
hasCodePushUpDependency,
hasScript,
Expand All @@ -10,12 +10,14 @@ import type { MonorepoToolHandler } from '../tools.js';

export const yarnHandler: MonorepoToolHandler = {
tool: 'yarn',

async isConfigured(options) {
return (
(await fileExists(join(options.cwd, 'yarn.lock'))) &&
(await hasWorkspacesEnabled(options.cwd))
);
},

async listProjects(options) {
const { workspaces, rootPackageJson } = await listWorkspaces(options.cwd);
return workspaces
Expand All @@ -32,4 +34,26 @@ export const yarnHandler: MonorepoToolHandler = {
: `yarn workspace ${name} exec ${options.task}`,
}));
},

async createRunManyCommand(options, onlyProjects) {
const { stdout } = await executeProcess({ command: 'yarn', args: ['-v'] });
const isV1 = stdout.startsWith('1.');

if (isV1) {
// neither parallel execution nor projects filter are supported in Yarn v1
return `yarn workspaces run ${options.task}`;
}

return [
'yarn',
'workspaces',
'foreach',
...(options.parallel ? ['--parallel'] : []),
...(typeof options.parallel === 'number'
? [`--jobs=${options.parallel}`]
: []),
...(onlyProjects?.map(project => `--include=${project}`) ?? ['--all']),
options.task,
].join(' ');
},
};
72 changes: 49 additions & 23 deletions packages/ci/src/lib/monorepo/list-projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,55 +4,80 @@ import type { Logger, Settings } from '../models.js';
import { detectMonorepoTool } from './detect-tool.js';
import { getToolHandler } from './handlers/index.js';
import { listPackages } from './packages.js';
import type { MonorepoHandlerOptions, ProjectConfig } from './tools.js';
import type {
MonorepoHandlerOptions,
MonorepoTool,
ProjectConfig,
} from './tools.js';

export type MonorepoProjects = {
tool: MonorepoTool | null;
projects: ProjectConfig[];
runManyCommand?: (onlyProjects?: string[]) => string | Promise<string>;
};

export async function listMonorepoProjects(
settings: Settings,
): Promise<ProjectConfig[]> {
if (!settings.monorepo) {
throw new Error('Monorepo mode not enabled');
}

): Promise<MonorepoProjects> {
const logger = settings.logger;

const options = createMonorepoHandlerOptions(settings);

const tool =
settings.monorepo === true
? await detectMonorepoTool(options)
: settings.monorepo;
if (settings.monorepo === true) {
if (tool) {
logger.info(`Auto-detected monorepo tool ${tool}`);
} else {
logger.info("Couldn't auto-detect any supported monorepo tool");
}
} else {
logger.info(`Using monorepo tool "${tool}" from inputs`);
}
const tool = await resolveMonorepoTool(settings, options);

if (tool) {
const handler = getToolHandler(tool);
const projects = await handler.listProjects(options);
logger.info(`Found ${projects.length} projects in ${tool} monorepo`);
logger.debug(`Projects: ${projects.map(({ name }) => name).join(', ')}`);
return projects;
return {
tool,
projects,
runManyCommand: onlyProjects =>
handler.createRunManyCommand(options, onlyProjects),
};
}

if (settings.projects) {
return listProjectsByGlobs({
const projects = await listProjectsByGlobs({
patterns: settings.projects,
cwd: options.cwd,
bin: settings.bin,
logger,
});
return { tool, projects };
}

return listProjectsByNpmPackages({
const projects = await listProjectsByNpmPackages({
cwd: options.cwd,
bin: settings.bin,
logger,
});
return { tool, projects };
}

async function resolveMonorepoTool(
settings: Settings,
options: MonorepoHandlerOptions,
): Promise<MonorepoTool | null> {
if (!settings.monorepo) {
// shouldn't happen, handled by caller
throw new Error('Monorepo mode not enabled');
}
const logger = settings.logger;

if (typeof settings.monorepo === 'string') {
logger.info(`Using monorepo tool "${settings.monorepo}" from inputs`);
return settings.monorepo;
}

const tool = await detectMonorepoTool(options);
if (tool) {
logger.info(`Auto-detected monorepo tool ${tool}`);
} else {
logger.info("Couldn't auto-detect any supported monorepo tool");
}

return tool;
}

function createMonorepoHandlerOptions(
Expand All @@ -61,6 +86,7 @@ function createMonorepoHandlerOptions(
return {
task: settings.task,
cwd: settings.directory,
parallel: false, // TODO: add to settings
nxProjectsFilter: settings.nxProjectsFilter,
...(!settings.silent && {
observer: {
Expand Down
Loading

0 comments on commit 094797d

Please sign in to comment.