Skip to content
8 changes: 5 additions & 3 deletions e2e/cli-e2e/tests/__snapshots__/help.e2e.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,14 @@ Global Options:
[array] [default: []]

Persist Options:
--persist.outputDir Directory for the produced reports
--persist.outputDir Directory for the produced reports
[string]
--persist.filename Filename for the produced reports.
--persist.filename Filename for the produced reports.
[string]
--persist.format Format of the report output. e.g. \`md\`, \`json\`
--persist.format Format of the report output. e.g. \`md\`, \`json\`
[array]
--persist.skipReports Skip generating report files. (useful in combinatio
n with caching) [boolean]

Upload Options:
--upload.organization Organization slug from portal
Expand Down
24 changes: 23 additions & 1 deletion e2e/cli-e2e/tests/collect.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
TEST_OUTPUT_DIR,
teardownTestFolder,
} from '@code-pushup/test-utils';
import { executeProcess, readTextFile } from '@code-pushup/utils';
import { executeProcess, fileExists, readTextFile } from '@code-pushup/utils';

describe('CLI collect', () => {
const dummyPluginTitle = 'Dummy Plugin';
Expand Down Expand Up @@ -61,6 +61,28 @@ describe('CLI collect', () => {
expect(md).toContain(dummyAuditTitle);
});

it('should not create reports if --persist.skipReports is given', async () => {
const { code } = await executeProcess({
command: 'npx',
args: [
'@code-pushup/cli',
'--no-progress',
'collect',
'--persist.skipReports',
],
cwd: dummyDir,
});

expect(code).toBe(0);

await expect(
fileExists(path.join(dummyOutputDir, 'report.md')),
).resolves.toBeFalsy();
await expect(
fileExists(path.join(dummyOutputDir, 'report.json')),
).resolves.toBeFalsy();
});

it('should print report summary to stdout', async () => {
const { code, stdout } = await executeProcess({
command: 'npx',
Expand Down
1 change: 1 addition & 0 deletions packages/ci/src/lib/run-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,7 @@ export function configFromPatterns(
outputDir: interpolate(persist.outputDir, variables),
filename: interpolate(persist.filename, variables),
format: persist.format,
skipReports: persist.skipReports,
},
...(upload && {
upload: {
Expand Down
1 change: 1 addition & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ Each example is fully tested to demonstrate best practices for plugin testing as
| **`--persist.outputDir`** | `string` | n/a | Directory for the produced reports. |
| **`--persist.filename`** | `string` | `report` | Filename for the produced reports without extension. |
| **`--persist.format`** | `('json' \| 'md')[]` | `json` | Format(s) of the report file. |
| **`--persist.skipReports`** | `boolean` | `false` | Skip generating report files. (useful in combination with caching) |
| **`--upload.organization`** | `string` | n/a | Organization slug from portal. |
| **`--upload.project`** | `string` | n/a | Project slug from portal. |
| **`--upload.server`** | `string` | n/a | URL to your portal server. |
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/lib/collect/collect-command.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ describe('collect-command', () => {
expect(collectAndPersistReports).toHaveBeenCalledWith(
expect.objectContaining({
config: '/test/code-pushup.config.ts',
persist: expect.objectContaining<Required<PersistConfig>>({
persist: expect.objectContaining<PersistConfig>({
filename: DEFAULT_PERSIST_FILENAME,
outputDir: DEFAULT_PERSIST_OUTPUT_DIR,
format: DEFAULT_PERSIST_FORMAT,
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/lib/compare/compare-command.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ describe('compare-command', () => {
outputDir: DEFAULT_PERSIST_OUTPUT_DIR,
filename: DEFAULT_PERSIST_FILENAME,
format: DEFAULT_PERSIST_FORMAT,
skipReports: false,
},
upload: expect.any(Object),
},
Expand Down
5 changes: 5 additions & 0 deletions packages/cli/src/lib/implementation/core-config.int.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ describe('parsing values from CLI and middleware', () => {
filename: DEFAULT_PERSIST_FILENAME,
format: DEFAULT_PERSIST_FORMAT,
outputDir: DEFAULT_PERSIST_OUTPUT_DIR,
skipReports: false,
});
});

Expand All @@ -85,6 +86,7 @@ describe('parsing values from CLI and middleware', () => {
filename: 'cli-filename',
format: ['md'],
outputDir: 'cli-outputDir',
skipReports: false,
});
});

Expand All @@ -101,6 +103,7 @@ describe('parsing values from CLI and middleware', () => {
filename: 'rc-filename',
format: ['json', 'md'],
outputDir: 'rc-outputDir',
skipReports: false,
});
});

Expand All @@ -122,6 +125,7 @@ describe('parsing values from CLI and middleware', () => {
filename: 'cli-filename',
format: ['md'],
outputDir: 'cli-outputDir',
skipReports: false,
});
});

Expand All @@ -141,6 +145,7 @@ describe('parsing values from CLI and middleware', () => {
filename: 'rc-filename',
format: DEFAULT_PERSIST_FORMAT,
outputDir: 'cli-outputdir',
skipReports: false,
});
});

Expand Down
41 changes: 30 additions & 11 deletions packages/cli/src/lib/implementation/core-config.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,24 @@ export type CoreConfigMiddlewareOptions = GeneralCliOptions &
CoreConfigCliOptions &
FilterOptions;

function buildPersistConfig(
cliPersist: CoreConfigCliOptions['persist'],
rcPersist: CoreConfig['persist'],
): Required<CoreConfig['persist']> {
return {
outputDir:
cliPersist?.outputDir ??
rcPersist?.outputDir ??
DEFAULT_PERSIST_OUTPUT_DIR,
filename:
cliPersist?.filename ?? rcPersist?.filename ?? DEFAULT_PERSIST_FILENAME,
format: normalizeFormats(
cliPersist?.format ?? rcPersist?.format ?? DEFAULT_PERSIST_FORMAT,
),
skipReports: cliPersist?.skipReports ?? rcPersist?.skipReports ?? false,
};
}

export async function coreConfigMiddleware<
T extends CoreConfigMiddlewareOptions,
>(processArgs: T): Promise<GeneralCliOptions & CoreConfig & FilterOptions> {
Expand Down Expand Up @@ -43,22 +61,23 @@ export async function coreConfigMiddleware<
});
return {
...(config != null && { config }),
persist: {
outputDir:
cliPersist?.outputDir ??
rcPersist?.outputDir ??
DEFAULT_PERSIST_OUTPUT_DIR,
filename:
cliPersist?.filename ?? rcPersist?.filename ?? DEFAULT_PERSIST_FILENAME,
format: normalizeFormats(
cliPersist?.format ?? rcPersist?.format ?? DEFAULT_PERSIST_FORMAT,
),
},
persist: buildPersistConfig(cliPersist, rcPersist),
...(upload != null && { upload }),
...remainingRcConfig,
...remainingCliOptions,
};
}

export const normalizeBooleanWithNegation = <T extends string>(
propertyName: T,
cliOptions?: Record<T, unknown>,
rcOptions?: Record<T, unknown>,
): boolean =>
propertyName in (cliOptions ?? {})
? (cliOptions?.[propertyName] as boolean)
: `no-${propertyName}` in (cliOptions ?? {})
? false
: ((rcOptions?.[propertyName] as boolean) ?? true);

export const normalizeFormats = (formats?: string[]): Format[] =>
(formats ?? []).flatMap(format => format.split(',') as Format[]);
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, expect, vi } from 'vitest';
import { autoloadRc, readRcByPath } from '@code-pushup/core';
import {
coreConfigMiddleware,
normalizeBooleanWithNegation,
normalizeFormats,
} from './core-config.middleware.js';
import type { CoreConfigCliOptions } from './core-config.model.js';
Expand All @@ -19,6 +20,36 @@ vi.mock('@code-pushup/core', async () => {
};
});

describe('normalizeBooleanWithNegation', () => {
it('should return true when CLI property is true', () => {
expect(normalizeBooleanWithNegation('report', { report: true }, {})).toBe(
true,
);
});

it('should return false when CLI property is false', () => {
expect(normalizeBooleanWithNegation('report', { report: false }, {})).toBe(
false,
);
});

it('should return false when no-property exists in CLI persist', () => {
expect(
normalizeBooleanWithNegation('report', { 'no-report': true }, {}),
).toBe(false);
});

it('should fallback to RC persist when no CLI property', () => {
expect(normalizeBooleanWithNegation('report', {}, { report: false })).toBe(
false,
);
});

it('should return default true when no property anywhere', () => {
expect(normalizeBooleanWithNegation('report', {}, {})).toBe(true);
});
});

describe('normalizeFormats', () => {
it('should forward valid formats', () => {
expect(normalizeFormats(['json', 'md'])).toEqual(['json', 'md']);
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/lib/implementation/core-config.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export type PersistConfigCliOptions = {
'persist.outputDir'?: string;
'persist.filename'?: string;
'persist.format'?: Format;
'persist.skipReports'?: boolean;
};

export type UploadConfigCliOptions = {
Expand Down
5 changes: 5 additions & 0 deletions packages/cli/src/lib/implementation/core-config.options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ export function yargsPersistConfigOptionsDefinition(): Record<
describe: 'Format of the report output. e.g. `md`, `json`',
type: 'array',
},
'persist.skipReports': {
describe:
'Skip generating report files. (useful in combination with caching)',
type: 'boolean',
},
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ describe('merge-diffs-command', () => {
outputDir: DEFAULT_PERSIST_OUTPUT_DIR,
filename: DEFAULT_PERSIST_FILENAME,
format: DEFAULT_PERSIST_FORMAT,
skipReports: false,
},
);
});
Expand Down
38 changes: 25 additions & 13 deletions packages/core/src/lib/collect-and-persist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
logStdoutSummary,
scoreReport,
sortReport,
ui,
} from '@code-pushup/utils';
import { collect } from './implementation/collect.js';
import {
Expand All @@ -19,29 +20,40 @@ import type { GlobalOptions } from './types.js';
export type CollectAndPersistReportsOptions = Pick<
CoreConfig,
'plugins' | 'categories'
> & { persist: Required<PersistConfig> } & Partial<GlobalOptions>;
> & {
persist: Required<Omit<PersistConfig, 'skipReports'>> &
Pick<PersistConfig, 'skipReports'>;
} & Partial<GlobalOptions>;

export async function collectAndPersistReports(
options: CollectAndPersistReportsOptions,
): Promise<void> {
const report = await collect(options);
const sortedScoredReport = sortReport(scoreReport(report));
const logger = ui().logger;
const reportResult = await collect(options);
const sortedScoredReport = sortReport(scoreReport(reportResult));

const persistResults = await persistReport(
report,
sortedScoredReport,
options.persist,
);
const { persist } = options;
const { skipReports = false, ...persistOptions } = persist ?? {};

// terminal output
logStdoutSummary(sortedScoredReport);
if (skipReports === true) {
logger.info('Skipping saving reports as `persist.skipReports` is true');
} else {
const persistResults = await persistReport(
reportResult,
sortedScoredReport,
persistOptions,
);

if (isVerbose()) {
logPersistedResults(persistResults);
if (isVerbose()) {
logPersistedResults(persistResults);
}
}

// terminal output
logStdoutSummary(sortedScoredReport);

// validate report and throw if invalid
report.plugins.forEach(plugin => {
reportResult.plugins.forEach(plugin => {
// Running checks after persisting helps while debugging as you can check the invalid output after the error is thrown
pluginReportSchema.parse(plugin);
});
Expand Down
25 changes: 25 additions & 0 deletions packages/core/src/lib/collect-and-persist.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,31 @@ describe('collectAndPersistReports', () => {
expect(logPersistedResults).toHaveBeenCalled();
});

it('should call collect and not persistReport if skipReports options is true in verbose mode', async () => {
const sortedScoredReport = sortReport(scoreReport(MINIMAL_REPORT_MOCK));

vi.stubEnv('CP_VERBOSE', 'true');

const verboseConfig: CollectAndPersistReportsOptions = {
...MINIMAL_CONFIG_MOCK,
persist: {
outputDir: 'output',
filename: 'report',
format: ['md'],
skipReports: true,
},
progress: false,
};
await collectAndPersistReports(verboseConfig);

expect(collect).toHaveBeenCalledWith(verboseConfig);

expect(persistReport).not.toHaveBeenCalled();
expect(logPersistedResults).not.toHaveBeenCalled();

expect(logStdoutSummary).toHaveBeenCalledWith(sortedScoredReport);
});

it('should print a summary to stdout', async () => {
await collectAndPersistReports(
MINIMAL_CONFIG_MOCK as CollectAndPersistReportsOptions,
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/lib/implementation/persist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export class PersistError extends Error {
export async function persistReport(
report: Report,
sortedScoredReport: ScoredReport,
options: Required<PersistConfig>,
options: Required<Omit<PersistConfig, 'skipReports'>>,
): Promise<MultipleFileResults> {
const { outputDir, filename, format } = options;

Expand Down
1 change: 1 addition & 0 deletions packages/models/src/lib/persist-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const persistConfigSchema = z.object({
.describe('Artifacts file name (without extension)')
.optional(),
format: z.array(formatSchema).optional(),
skipReports: z.boolean().optional(),
});

export type PersistConfig = z.infer<typeof persistConfigSchema>;
2 changes: 1 addition & 1 deletion packages/utils/src/lib/file-system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export function createReportPath({
filename,
format,
suffix,
}: Omit<Required<PersistConfig>, 'format'> & {
}: Pick<Required<PersistConfig>, 'filename' | 'outputDir'> & {
format: Format;
suffix?: string;
}): string {
Expand Down
Loading