From 68653ee354b7ec359bda5e9b0262311ca2e04511 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Chalk?= Date: Mon, 25 Nov 2024 10:29:24 +0100 Subject: [PATCH 1/2] feat(ci): accept custom output directory, with project name interpolation --- packages/ci/README.md | 25 +++++++++++-------- packages/ci/src/lib/cli/commands/collect.ts | 5 ++-- packages/ci/src/lib/cli/commands/compare.ts | 6 ++--- .../ci/src/lib/cli/commands/merge-diffs.ts | 11 +++++--- packages/ci/src/lib/cli/context.ts | 3 ++- packages/ci/src/lib/cli/persist.ts | 23 +++++++++++------ packages/ci/src/lib/constants.ts | 2 ++ packages/ci/src/lib/models.ts | 1 + 8 files changed, 48 insertions(+), 28 deletions(-) diff --git a/packages/ci/README.md b/packages/ci/README.md index c0522c5aa..7bccec24f 100644 --- a/packages/ci/README.md +++ b/packages/ci/README.md @@ -94,20 +94,23 @@ A `Comment` object has the following required properties: Optionally, you can override default options for further customization: -| Property | Type | Default | Description | -| :---------------- | :------------------------ | :------------------------------- | :-------------------------------------------------------------------------------- | -| `monorepo` | `boolean \| MonorepoTool` | `false` | Enables [monorepo mode](#monorepo-mode) | -| `projects` | `string[] \| null` | `null` | Custom projects configuration for [monorepo mode](#monorepo-mode) | -| `task` | `string` | `'code-pushup'` | Name of command to run Code PushUp per project in [monorepo mode](#monorepo-mode) | -| `directory` | `string` | `process.cwd()` | Directory in which Code PushUp CLI should run | -| `config` | `string \| null` | `null` [^1] | Path to config file (`--config` option) | -| `silent` | `boolean` | `false` | Toggles if logs from CLI commands are printed | -| `bin` | `string` | `'npx --no-install code-pushup'` | Command for executing Code PushUp CLI | -| `detectNewIssues` | `boolean` | `true` | Toggles if new issues should be detected and returned in `newIssues` property | -| `logger` | `Logger` | `console` | Logger for reporting progress and encountered problems | +| Property | Type | Default | Description | +| :---------------- | :------------------------ | :------------------------------- | :----------------------------------------------------------------------------------- | +| `monorepo` | `boolean \| MonorepoTool` | `false` | Enables [monorepo mode](#monorepo-mode) | +| `projects` | `string[] \| null` | `null` | Custom projects configuration for [monorepo mode](#monorepo-mode) | +| `task` | `string` | `'code-pushup'` | Name of command to run Code PushUp per project in [monorepo mode](#monorepo-mode) | +| `directory` | `string` | `process.cwd()` | Directory in which Code PushUp CLI should run | +| `config` | `string \| null` | `null` [^1] | Path to config file (`--config` option) | +| `silent` | `boolean` | `false` | Toggles if logs from CLI commands are printed | +| `bin` | `string` | `'npx --no-install code-pushup'` | Command for executing Code PushUp CLI | +| `detectNewIssues` | `boolean` | `true` | Toggles if new issues should be detected and returned in `newIssues` property | +| `logger` | `Logger` | `console` | Logger for reporting progress and encountered problems | +| `output` | `string` | `'.code-pushup'` | Directory where Code PushUp reports will be created (interpolates project name [^2]) | [^1]: By default, the `code-pushup.config` file is autodetected as described in [`@code-pushup/cli` docs](../cli/README.md#configuration). +[^2]: In monorepo mode, any occurrence of `{project}` in the `output` path will be replaced with a project name. This separation of folders per project (e.g. `output: '.code-pushup/{project}'`) may be useful for caching purposes. + The `Logger` object has the following required properties: | Property | Type | Description | diff --git a/packages/ci/src/lib/cli/commands/collect.ts b/packages/ci/src/lib/cli/commands/collect.ts index cdcc0ceaa..40620b28b 100644 --- a/packages/ci/src/lib/cli/commands/collect.ts +++ b/packages/ci/src/lib/cli/commands/collect.ts @@ -12,12 +12,13 @@ export async function runCollect({ directory, silent, project, + output, }: CommandContext): Promise { const { stdout } = await executeProcess({ command: bin, args: [ ...(config ? [`--config=${config}`] : []), - ...persistCliOptions({ directory, project }), + ...persistCliOptions({ directory, project, output }), ], cwd: directory, }); @@ -25,5 +26,5 @@ export async function runCollect({ console.info(stdout); } - return persistedCliFiles({ directory, project }); + return persistedCliFiles({ directory, project, output }); } diff --git a/packages/ci/src/lib/cli/commands/compare.ts b/packages/ci/src/lib/cli/commands/compare.ts index 4bb8288dd..89ddf3ec7 100644 --- a/packages/ci/src/lib/cli/commands/compare.ts +++ b/packages/ci/src/lib/cli/commands/compare.ts @@ -14,7 +14,7 @@ type CompareOptions = { export async function runCompare( { before, after, label }: CompareOptions, - { bin, config, directory, silent, project }: CommandContext, + { bin, config, directory, silent, project, output }: CommandContext, ): Promise { const { stdout } = await executeProcess({ command: bin, @@ -24,7 +24,7 @@ export async function runCompare( `--after=${after}`, ...(label ? [`--label=${label}`] : []), ...(config ? [`--config=${config}`] : []), - ...persistCliOptions({ directory, project }), + ...persistCliOptions({ directory, project, output }), ], cwd: directory, }); @@ -32,5 +32,5 @@ export async function runCompare( console.info(stdout); } - return persistedCliFiles({ directory, isDiff: true, project }); + return persistedCliFiles({ directory, isDiff: true, project, output }); } diff --git a/packages/ci/src/lib/cli/commands/merge-diffs.ts b/packages/ci/src/lib/cli/commands/merge-diffs.ts index 8e21a4ca6..fb2d0bcca 100644 --- a/packages/ci/src/lib/cli/commands/merge-diffs.ts +++ b/packages/ci/src/lib/cli/commands/merge-diffs.ts @@ -8,7 +8,7 @@ import { export async function runMergeDiffs( files: string[], - { bin, config, directory, silent }: CommandContext, + { bin, config, directory, silent, output }: CommandContext, ): Promise> { const { stdout } = await executeProcess({ command: bin, @@ -16,7 +16,7 @@ export async function runMergeDiffs( 'merge-diffs', ...files.map(file => `--files=${file}`), ...(config ? [`--config=${config}`] : []), - ...persistCliOptions({ directory }), + ...persistCliOptions({ directory, output }), ], cwd: directory, }); @@ -24,5 +24,10 @@ export async function runMergeDiffs( console.info(stdout); } - return persistedCliFiles({ directory, isDiff: true, formats: ['md'] }); + return persistedCliFiles({ + directory, + isDiff: true, + formats: ['md'], + output, + }); } diff --git a/packages/ci/src/lib/cli/context.ts b/packages/ci/src/lib/cli/context.ts index 043519a20..746028ebb 100644 --- a/packages/ci/src/lib/cli/context.ts +++ b/packages/ci/src/lib/cli/context.ts @@ -3,7 +3,7 @@ import type { ProjectConfig } from '../monorepo'; export type CommandContext = Pick< Settings, - 'bin' | 'config' | 'directory' | 'silent' + 'bin' | 'config' | 'directory' | 'silent' | 'output' > & { project?: string; }; @@ -18,5 +18,6 @@ export function createCommandContext( directory: project?.directory ?? settings.directory, config: settings.config, silent: settings.silent, + output: settings.output.replaceAll('{project}', project?.name ?? ''), }; } diff --git a/packages/ci/src/lib/cli/persist.ts b/packages/ci/src/lib/cli/persist.ts index f9106f5e3..80326e447 100644 --- a/packages/ci/src/lib/cli/persist.ts +++ b/packages/ci/src/lib/cli/persist.ts @@ -2,7 +2,6 @@ import path from 'node:path'; import { DEFAULT_PERSIST_FILENAME, DEFAULT_PERSIST_FORMAT, - DEFAULT_PERSIST_OUTPUT_DIR, type Format, } from '@code-pushup/models'; import { projectToFilename } from '@code-pushup/utils'; @@ -22,12 +21,14 @@ export type PersistedCliFilesFormats = { export function persistCliOptions({ directory, project, + output, }: { directory: string; project?: string; + output: string; }): string[] { return [ - `--persist.outputDir=${path.join(directory, DEFAULT_PERSIST_OUTPUT_DIR)}`, + `--persist.outputDir=${path.join(directory, output)}`, `--persist.filename=${createFilename(project)}`, ...DEFAULT_PERSIST_FORMAT.map(format => `--persist.format=${format}`), ]; @@ -38,13 +39,15 @@ export function persistedCliFiles({ isDiff, project, formats, + output, }: { directory: string; isDiff?: boolean; project?: string; formats?: TFormat[]; + output: string; }): PersistedCliFiles { - const rootDir = path.join(directory, DEFAULT_PERSIST_OUTPUT_DIR); + const rootDir = path.join(directory, output); const filename = isDiff ? `${createFilename(project)}-diff` : createFilename(project); @@ -67,11 +70,15 @@ export function persistedCliFiles({ }; } -export function findPersistedFiles( - rootDir: string, - files: string[], - project?: string, -): PersistedCliFiles { +export function findPersistedFiles({ + rootDir, + files, + project, +}: { + rootDir: string; + files: string[]; + project?: string; +}): PersistedCliFiles { const filename = createFilename(project); const filePaths = DEFAULT_PERSIST_FORMAT.reduce((acc, format) => { const matchedFile = files.find(file => file === `${filename}.${format}`); diff --git a/packages/ci/src/lib/constants.ts b/packages/ci/src/lib/constants.ts index a0b9c385b..b3d5d8396 100644 --- a/packages/ci/src/lib/constants.ts +++ b/packages/ci/src/lib/constants.ts @@ -1,3 +1,4 @@ +import { DEFAULT_PERSIST_OUTPUT_DIR } from '@code-pushup/models'; import type { Settings } from './models'; export const DEFAULT_SETTINGS: Settings = { @@ -11,4 +12,5 @@ export const DEFAULT_SETTINGS: Settings = { debug: false, detectNewIssues: true, logger: console, + output: DEFAULT_PERSIST_OUTPUT_DIR, }; diff --git a/packages/ci/src/lib/models.ts b/packages/ci/src/lib/models.ts index 258a1101a..724f4322e 100644 --- a/packages/ci/src/lib/models.ts +++ b/packages/ci/src/lib/models.ts @@ -16,6 +16,7 @@ export type Options = { debug?: boolean; detectNewIssues?: boolean; logger?: Logger; + output?: string; }; /** From eb6cbbf049be271cd1f3c3051b6d223182510994 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Chalk?= Date: Mon, 25 Nov 2024 12:08:36 +0100 Subject: [PATCH 2/2] test(ci): add missing unit tests for CLI helpers --- packages/ci/src/lib/cli/context.unit.test.ts | 101 ++++++++++++ packages/ci/src/lib/cli/persist.unit.test.ts | 152 +++++++++++++++++++ 2 files changed, 253 insertions(+) create mode 100644 packages/ci/src/lib/cli/context.unit.test.ts create mode 100644 packages/ci/src/lib/cli/persist.unit.test.ts diff --git a/packages/ci/src/lib/cli/context.unit.test.ts b/packages/ci/src/lib/cli/context.unit.test.ts new file mode 100644 index 000000000..bdd2e8356 --- /dev/null +++ b/packages/ci/src/lib/cli/context.unit.test.ts @@ -0,0 +1,101 @@ +import { DEFAULT_SETTINGS } from '../constants'; +import { type CommandContext, createCommandContext } from './context'; + +describe('createCommandContext', () => { + it('should pick CLI-related settings in standalone mode', () => { + expect( + createCommandContext( + { + bin: 'npx --no-install code-pushup', + config: null, + debug: false, + detectNewIssues: true, + directory: '/test', + logger: console, + monorepo: false, + output: '.code-pushup', + projects: null, + silent: false, + task: 'code-pushup', + }, + null, + ), + ).toStrictEqual({ + project: undefined, + bin: 'npx --no-install code-pushup', + directory: '/test', + config: null, + silent: false, + output: '.code-pushup', + }); + }); + + it('should override some settings when given monorepo project config', () => { + expect( + createCommandContext( + { + bin: 'npx --no-install code-pushup', + config: null, + debug: false, + detectNewIssues: true, + directory: '/test', + logger: console, + monorepo: false, + output: '.code-pushup', + projects: null, + silent: false, + task: 'code-pushup', + }, + { + name: 'ui', + directory: '/test/ui', + bin: 'yarn code-pushup', + }, + ), + ).toStrictEqual({ + project: 'ui', + bin: 'yarn code-pushup', + directory: '/test/ui', + config: null, + silent: false, + output: '.code-pushup', + }); + }); + + it('should interpolate project name in output path for monorepo project', () => { + expect( + createCommandContext( + { + ...DEFAULT_SETTINGS, + output: '.code-pushup/{project}', + }, + { + name: 'website', + bin: 'npx nx run website:code-pushup --', + }, + ), + ).toEqual( + expect.objectContaining>({ + project: 'website', + bin: 'npx nx run website:code-pushup --', + output: '.code-pushup/website', + }), + ); + }); + + it('should omit {project} placeholder in output path when in standalone mode', () => { + expect( + createCommandContext( + { + ...DEFAULT_SETTINGS, + output: '.code-pushup/{project}', + }, + undefined, + ), + ).toEqual( + expect.objectContaining>({ + output: '.code-pushup/', + }), + ); + }); +}); diff --git a/packages/ci/src/lib/cli/persist.unit.test.ts b/packages/ci/src/lib/cli/persist.unit.test.ts new file mode 100644 index 000000000..48cf96592 --- /dev/null +++ b/packages/ci/src/lib/cli/persist.unit.test.ts @@ -0,0 +1,152 @@ +import { join } from 'node:path'; +import { + type PersistedCliFiles, + findPersistedFiles, + persistCliOptions, + persistedCliFiles, +} from './persist'; + +describe('persistCliOptions', () => { + it('should create CLI arguments for standalone project', () => { + expect( + persistCliOptions({ + directory: process.cwd(), + output: '.code-pushup', + }), + ).toEqual([ + `--persist.outputDir=${join(process.cwd(), '.code-pushup')}`, + '--persist.filename=report', + '--persist.format=json', + '--persist.format=md', + ]); + }); + + it('should create CLI arguments for monorepo project', () => { + expect( + persistCliOptions({ + project: 'utils', + directory: process.cwd(), + output: '.code-pushup', + }), + ).toEqual([ + `--persist.outputDir=${join(process.cwd(), '.code-pushup')}`, + '--persist.filename=utils-report', + '--persist.format=json', + '--persist.format=md', + ]); + }); +}); + +describe('persistedCliFiles', () => { + it('should determine persisted files for standalone report', () => { + expect( + persistedCliFiles({ + directory: process.cwd(), + output: '.code-pushup', + }), + ).toEqual({ + jsonFilePath: join(process.cwd(), '.code-pushup/report.json'), + mdFilePath: join(process.cwd(), '.code-pushup/report.md'), + artifactData: { + rootDir: join(process.cwd(), '.code-pushup'), + files: [ + join(process.cwd(), '.code-pushup/report.json'), + join(process.cwd(), '.code-pushup/report.md'), + ], + }, + }); + }); + + it('should determine persisted files for monorepo report', () => { + expect( + persistedCliFiles({ + directory: process.cwd(), + output: '.code-pushup/auth', + project: 'auth', + }), + ).toEqual({ + jsonFilePath: join(process.cwd(), '.code-pushup/auth/auth-report.json'), + mdFilePath: join(process.cwd(), '.code-pushup/auth/auth-report.md'), + artifactData: { + rootDir: join(process.cwd(), '.code-pushup/auth'), + files: [ + join(process.cwd(), '.code-pushup/auth/auth-report.json'), + join(process.cwd(), '.code-pushup/auth/auth-report.md'), + ], + }, + }); + }); + + it('should determine persisted files for diff in Markdown format only', () => { + expect( + persistedCliFiles({ + directory: process.cwd(), + output: '.code-pushup', + isDiff: true, + formats: ['md'], + }), + ).toEqual>({ + mdFilePath: join(process.cwd(), '.code-pushup/report-diff.md'), + artifactData: { + rootDir: join(process.cwd(), '.code-pushup'), + files: [join(process.cwd(), '.code-pushup/report-diff.md')], + }, + }); + }); +}); + +describe('findPersistedFiles', () => { + it('should find report files in artifact data for standalone project', () => { + expect( + findPersistedFiles({ + rootDir: join(process.cwd(), '.code-pushup'), + files: [ + 'report-diff.json', + 'report-diff.md', + 'report.json', + 'report.md', + ], + }), + ).toEqual({ + jsonFilePath: join(process.cwd(), '.code-pushup/report.json'), + mdFilePath: join(process.cwd(), '.code-pushup/report.md'), + artifactData: { + rootDir: join(process.cwd(), '.code-pushup'), + files: [ + join(process.cwd(), '.code-pushup/report.json'), + join(process.cwd(), '.code-pushup/report.md'), + ], + }, + }); + }); + + it('should find report files in artifact data for monorepo project', () => { + expect( + findPersistedFiles({ + rootDir: join(process.cwd(), '.code-pushup'), + files: [ + 'backend-report-diff.json', + 'backend-report-diff.md', + 'backend-report.json', + 'backend-report.md', + 'frontend-report-diff.json', + 'frontend-report-diff.md', + 'frontend-report.json', + 'frontend-report.md', + 'report-diff.md', + ], + project: 'frontend', + }), + ).toEqual({ + jsonFilePath: join(process.cwd(), '.code-pushup/frontend-report.json'), + mdFilePath: join(process.cwd(), '.code-pushup/frontend-report.md'), + artifactData: { + rootDir: join(process.cwd(), '.code-pushup'), + files: [ + join(process.cwd(), '.code-pushup/frontend-report.json'), + join(process.cwd(), '.code-pushup/frontend-report.md'), + ], + }, + }); + }); +});