From 0462f5a92b5132e11e8378f2380b1e3273dc8e15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Chalk?= Date: Wed, 3 Sep 2025 17:20:59 +0200 Subject: [PATCH 1/3] feat(models,core): make runner args extensible by nesting persist config --- .../core/src/lib/implementation/collect.ts | 21 ++++------ .../src/lib/implementation/execute-plugin.ts | 41 ++++++++++--------- .../execute-plugin.unit.test.ts | 12 ++++-- .../core/src/lib/implementation/runner.ts | 22 +++++----- packages/models/docs/models-reference.md | 18 +++++++- packages/models/src/index.ts | 3 ++ packages/models/src/lib/runner-config.ts | 18 +++++--- .../plugin-eslint/src/lib/runner.int.test.ts | 10 ++--- .../plugin-eslint/src/lib/runner/index.ts | 8 +--- .../src/lib/runner/index.unit.test.ts | 15 ++++--- 10 files changed, 94 insertions(+), 74 deletions(-) diff --git a/packages/core/src/lib/implementation/collect.ts b/packages/core/src/lib/implementation/collect.ts index 9a07800ea..9efa4a311 100644 --- a/packages/core/src/lib/implementation/collect.ts +++ b/packages/core/src/lib/implementation/collect.ts @@ -1,17 +1,16 @@ import { createRequire } from 'node:module'; -import { - type CacheConfigObject, - type CoreConfig, - DEFAULT_PERSIST_OUTPUT_DIR, - type PersistConfig, - type Report, +import type { + CacheConfigObject, + CoreConfig, + PersistConfig, + Report, } from '@code-pushup/models'; import { calcDuration, getLatestCommit } from '@code-pushup/utils'; import type { GlobalOptions } from '../types.js'; import { executePlugins } from './execute-plugin.js'; export type CollectOptions = Pick & { - persist?: Required>; + persist?: PersistConfig; cache: CacheConfigObject; } & Partial; @@ -20,16 +19,12 @@ export type CollectOptions = Pick & { * @param options */ export async function collect(options: CollectOptions): Promise { - const { plugins, categories, persist, cache, ...otherOptions } = options; + const { plugins, categories, persist = {}, cache, ...otherOptions } = options; const date = new Date().toISOString(); const start = performance.now(); const commit = await getLatestCommit(); const pluginOutputs = await executePlugins( - { - plugins, - persist: { outputDir: DEFAULT_PERSIST_OUTPUT_DIR, ...persist }, - cache, - }, + { plugins, persist, cache }, otherOptions, ); const packageJson = createRequire(import.meta.url)( diff --git a/packages/core/src/lib/implementation/execute-plugin.ts b/packages/core/src/lib/implementation/execute-plugin.ts index d61b8f17d..e492b4453 100644 --- a/packages/core/src/lib/implementation/execute-plugin.ts +++ b/packages/core/src/lib/implementation/execute-plugin.ts @@ -1,12 +1,13 @@ import { bold } from 'ansis'; -import type { - Audit, - AuditOutput, - AuditReport, - CacheConfigObject, - PersistConfig, - PluginConfig, - PluginReport, +import { + type AuditOutput, + type AuditReport, + type CacheConfigObject, + DEFAULT_PERSIST_CONFIG, + type PersistConfig, + type PluginConfig, + type PluginReport, + type RunnerArgs, } from '@code-pushup/models'; import { type ProgressBar, @@ -48,10 +49,9 @@ export async function executePlugin( pluginConfig: PluginConfig, opt: { cache: CacheConfigObject; - persist: Required>; + persist: PersistConfig; }, ): Promise { - const { cache, persist } = opt; const { runner, audits: pluginConfigAudits, @@ -61,15 +61,19 @@ export async function executePlugin( scoreTargets, ...pluginMeta } = pluginConfig; - const { write: cacheWrite = false, read: cacheRead = false } = cache; - const { outputDir } = persist; + const { write: cacheWrite = false, read: cacheRead = false } = opt.cache; + + const args: RunnerArgs = { + persist: { ...DEFAULT_PERSIST_CONFIG, ...opt.persist }, + }; + const { outputDir } = args.persist; const { audits, ...executionMeta } = cacheRead ? // IF not null, take the result from cache ((await readRunnerResults(pluginMeta.slug, outputDir)) ?? // ELSE execute the plugin runner - (await executePluginRunner(pluginConfig, persist))) - : await executePluginRunner(pluginConfig, persist); + (await executePluginRunner(pluginConfig, args))) + : await executePluginRunner(pluginConfig, args); if (cacheWrite) { await writeRunnerResults(pluginMeta.slug, outputDir, { @@ -87,9 +91,8 @@ export async function executePlugin( const auditReports: AuditReport[] = scoredAuditsWithTarget.map( (auditOutput: AuditOutput) => ({ ...auditOutput, - ...(pluginConfigAudits.find( - audit => audit.slug === auditOutput.slug, - ) as Audit), + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + ...pluginConfigAudits.find(audit => audit.slug === auditOutput.slug)!, }), ); @@ -107,7 +110,7 @@ export async function executePlugin( const wrapProgress = async ( cfg: { plugin: PluginConfig; - persist: Required>; + persist: PersistConfig; cache: CacheConfigObject; }, steps: number, @@ -155,7 +158,7 @@ const wrapProgress = async ( export async function executePlugins( cfg: { plugins: PluginConfig[]; - persist: Required>; + persist: PersistConfig; cache: CacheConfigObject; }, options?: { progress?: boolean }, diff --git a/packages/core/src/lib/implementation/execute-plugin.unit.test.ts b/packages/core/src/lib/implementation/execute-plugin.unit.test.ts index 191e2f995..b536cd7bd 100644 --- a/packages/core/src/lib/implementation/execute-plugin.unit.test.ts +++ b/packages/core/src/lib/implementation/execute-plugin.unit.test.ts @@ -1,7 +1,11 @@ import { bold } from 'ansis'; import { vol } from 'memfs'; import { describe, expect, it, vi } from 'vitest'; -import type { AuditOutputs, PluginConfig } from '@code-pushup/models'; +import { + type AuditOutputs, + DEFAULT_PERSIST_CONFIG, + type PluginConfig, +} from '@code-pushup/models'; import { MEMFS_VOLUME, MINIMAL_PLUGIN_CONFIG_MOCK, @@ -26,7 +30,7 @@ describe('executePlugin', () => { await expect( executePlugin(MINIMAL_PLUGIN_CONFIG_MOCK, { - persist: { outputDir: '' }, + persist: {}, cache: { read: false, write: false }, }), ).resolves.toStrictEqual({ @@ -47,7 +51,7 @@ describe('executePlugin', () => { expect(executePluginRunnerSpy).toHaveBeenCalledWith( MINIMAL_PLUGIN_CONFIG_MOCK, - { outputDir: '' }, + { persist: DEFAULT_PERSIST_CONFIG }, ); }); @@ -132,7 +136,7 @@ describe('executePlugin', () => { expect(executePluginRunnerSpy).toHaveBeenCalledWith( MINIMAL_PLUGIN_CONFIG_MOCK, - { outputDir: MEMFS_VOLUME }, + { persist: { ...DEFAULT_PERSIST_CONFIG, outputDir: MEMFS_VOLUME } }, ); }); diff --git a/packages/core/src/lib/implementation/runner.ts b/packages/core/src/lib/implementation/runner.ts index 0bec5a3b4..6d8d5989a 100644 --- a/packages/core/src/lib/implementation/runner.ts +++ b/packages/core/src/lib/implementation/runner.ts @@ -3,8 +3,8 @@ import { writeFile } from 'node:fs/promises'; import path from 'node:path'; import { type AuditOutputs, - type PersistConfig, type PluginConfig, + type RunnerArgs, type RunnerConfig, type RunnerFunction, auditOutputsSchema, @@ -33,14 +33,14 @@ export type ValidatedRunnerResult = Omit & { }; export async function executeRunnerConfig( - cfg: RunnerConfig, - config: Required>, + config: RunnerConfig, + args: RunnerArgs, ): Promise { - const { args, command, outputFile, outputTransform } = cfg; + const { outputFile, outputTransform } = config; const { duration, date } = await executeProcess({ - command, - args: [...(args ?? []), ...objectToCliArgs(config)], + command: config.command, + args: [...(config.args ?? []), ...objectToCliArgs(args)], observer: { onStdout: stdout => { if (isVerbose()) { @@ -69,13 +69,13 @@ export async function executeRunnerConfig( export async function executeRunnerFunction( runner: RunnerFunction, - config: PersistConfig, + args: RunnerArgs, ): Promise { const date = new Date().toISOString(); const start = performance.now(); // execute plugin runner - const audits = await runner(config); + const audits = await runner(args); // create runner result return { @@ -100,13 +100,13 @@ export class AuditOutputsMissingAuditError extends Error { export async function executePluginRunner( pluginConfig: Pick, - persist: Required>, + args: RunnerArgs, ): Promise & { audits: AuditOutputs }> { const { audits: pluginConfigAudits, runner } = pluginConfig; const runnerResult: RunnerResult = typeof runner === 'object' - ? await executeRunnerConfig(runner, persist) - : await executeRunnerFunction(runner, persist); + ? await executeRunnerConfig(runner, args) + : await executeRunnerFunction(runner, args); const { audits: unvalidatedAuditOutputs, ...executionMeta } = runnerResult; const result = auditOutputsSchema.safeParse(unvalidatedAuditOutputs); diff --git a/packages/models/docs/models-reference.md b/packages/models/docs/models-reference.md index c2aa5bac8..20b59db7f 100644 --- a/packages/models/docs/models-reference.md +++ b/packages/models/docs/models-reference.md @@ -1394,9 +1394,21 @@ _Object containing the following properties:_ _(\*) Required._ +## RunnerArgs + +Arguments passed to runner + +_Object containing the following properties:_ + +| Property | Description | Type | +| :----------------- | :----------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **`persist`** (\*) | Persist config with defaults applied | _Object with properties:_
  • **`outputDir`** (\*): `string` (_min length: 1_)
  • **`filename`** (\*): `string` (_regex: `/^(?!.*[ \\/:*?"<>\|]).+$/`, min length: 1_)
  • **`format`** (\*): _Array of [Format](#format) items_
  • **`skipReports`** (\*): `boolean`
| + +_(\*) Required._ + ## RunnerConfig -How to execute runner +How to execute runner using shell script _Object containing the following properties:_ @@ -1423,11 +1435,13 @@ _(\*) Required._ ## RunnerFunction +Callback function for async runner execution in JS/TS + _Function._ _Parameters:_ -1. [PersistConfig](#persistconfig) +1. [RunnerArgs](#runnerargs) _Returns:_ diff --git a/packages/models/src/index.ts b/packages/models/src/index.ts index 7e64d5403..2ccadd357 100644 --- a/packages/models/src/index.ts +++ b/packages/models/src/index.ts @@ -53,6 +53,7 @@ export { DEFAULT_PERSIST_FILENAME, DEFAULT_PERSIST_FORMAT, DEFAULT_PERSIST_OUTPUT_DIR, + DEFAULT_PERSIST_SKIP_REPORT, } from './lib/implementation/constants.js'; export { MAX_DESCRIPTION_LENGTH, @@ -116,9 +117,11 @@ export { type ReportsDiff, } from './lib/reports-diff.js'; export { + runnerArgsSchema, runnerConfigSchema, runnerFilesPathsSchema, runnerFunctionSchema, + type RunnerArgs, type RunnerConfig, type RunnerFilesPaths, type RunnerFunction, diff --git a/packages/models/src/lib/runner-config.ts b/packages/models/src/lib/runner-config.ts index 422b39639..04145f3de 100644 --- a/packages/models/src/lib/runner-config.ts +++ b/packages/models/src/lib/runner-config.ts @@ -12,6 +12,15 @@ export const outputTransformSchema = convertAsyncZodFunctionToSchema( ); export type OutputTransform = z.infer; +export const runnerArgsSchema = z + .object({ + persist: persistConfigSchema + .required() + .describe('Persist config with defaults applied'), + }) + .describe('Arguments passed to runner'); +export type RunnerArgs = z.infer; + export const runnerConfigSchema = z .object({ command: z.string().describe('Shell command to execute'), @@ -20,22 +29,19 @@ export const runnerConfigSchema = z outputTransform: outputTransformSchema.optional(), configFile: filePathSchema.describe('Runner config path').optional(), }) - .describe('How to execute runner'); - + .describe('How to execute runner using shell script'); export type RunnerConfig = z.infer; export const runnerFunctionSchema = convertAsyncZodFunctionToSchema( z.function({ - input: [persistConfigSchema], + input: [runnerArgsSchema], output: z.union([auditOutputsSchema, z.promise(auditOutputsSchema)]), }), -); - +).describe('Callback function for async runner execution in JS/TS'); export type RunnerFunction = z.infer; export const runnerFilesPathsSchema = z.object({ runnerConfigPath: filePathSchema.describe('Runner config path'), runnerOutputPath: filePathSchema.describe('Runner output path'), }); - export type RunnerFilesPaths = z.infer; diff --git a/packages/plugin-eslint/src/lib/runner.int.test.ts b/packages/plugin-eslint/src/lib/runner.int.test.ts index 05f8bb004..865724d9f 100644 --- a/packages/plugin-eslint/src/lib/runner.int.test.ts +++ b/packages/plugin-eslint/src/lib/runner.int.test.ts @@ -7,7 +7,7 @@ import { type Audit, type AuditOutput, type AuditOutputs, - DEFAULT_PERSIST_OUTPUT_DIR, + DEFAULT_PERSIST_CONFIG, type Issue, } from '@code-pushup/models'; import { osAgnosticAuditOutputs } from '@code-pushup/test-utils'; @@ -50,9 +50,9 @@ describe('executeRunner', () => { it('should execute ESLint and create audit results for React application', async () => { const args = await prepareRunnerArgs('eslint.config.js'); - const runnerFn = await createRunnerFunction(args); + const runnerFn = createRunnerFunction(args); const res = (await runnerFn({ - outputDir: DEFAULT_PERSIST_OUTPUT_DIR, + persist: DEFAULT_PERSIST_CONFIG, })) as AuditOutputs; expect(osAgnosticAuditOutputs(res)).toMatchSnapshot(); }); @@ -61,11 +61,11 @@ describe('executeRunner', () => { 'should execute runner with custom config using @code-pushup/eslint-config', async () => { const eslintTarget = 'code-pushup.eslint.config.mjs'; - const runnerFn = await createRunnerFunction({ + const runnerFn = createRunnerFunction({ ...(await prepareRunnerArgs(eslintTarget)), }); - const json = await runnerFn({ outputDir: DEFAULT_PERSIST_OUTPUT_DIR }); + const json = await runnerFn({ persist: DEFAULT_PERSIST_CONFIG }); // expect warnings from unicorn/filename-case rule from default config expect(json).toContainEqual( expect.objectContaining>({ diff --git a/packages/plugin-eslint/src/lib/runner/index.ts b/packages/plugin-eslint/src/lib/runner/index.ts index 79a4abdfc..5d95e9981 100644 --- a/packages/plugin-eslint/src/lib/runner/index.ts +++ b/packages/plugin-eslint/src/lib/runner/index.ts @@ -2,7 +2,6 @@ import type { Audit, AuditOutput, AuditOutputs, - PersistConfig, PluginArtifactOptions, RunnerFunction, } from '@code-pushup/models'; @@ -23,15 +22,12 @@ export function createRunnerFunction(options: { slugs: audits.map(audit => audit.slug), }; - return async ({ outputDir }: PersistConfig): Promise => { + return async (): Promise => { ui().logger.log(`ESLint plugin executing ${targets.length} lint targets`); const linterOutputs = artifacts ? await loadArtifacts(artifacts) - : await asyncSequential( - targets.map(target => ({ ...target, outputDir })), - lint, - ); + : await asyncSequential(targets, lint); const lintResults = mergeLinterOutputs(linterOutputs); const failedAudits = lintResultsToAudits(lintResults); diff --git a/packages/plugin-eslint/src/lib/runner/index.unit.test.ts b/packages/plugin-eslint/src/lib/runner/index.unit.test.ts index bc9413d56..52f38bef9 100644 --- a/packages/plugin-eslint/src/lib/runner/index.unit.test.ts +++ b/packages/plugin-eslint/src/lib/runner/index.unit.test.ts @@ -1,9 +1,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { z } from 'zod'; -import type { - Audit, - AuditOutput, - pluginArtifactOptionsSchema, +import { + type Audit, + type AuditOutput, + DEFAULT_PERSIST_CONFIG, + type pluginArtifactOptionsSchema, } from '@code-pushup/models'; import { ui } from '@code-pushup/utils'; import type { ESLintTarget } from '../config.js'; @@ -124,7 +125,7 @@ describe('createRunnerFunction', () => { audits: mockAudits, targets: mockTargets, artifacts, - })({}), + })({ persist: DEFAULT_PERSIST_CONFIG }), ).resolves.toStrictEqual(mockedAuditOutputs); expect(loadArtifactsSpy).toHaveBeenCalledWith(artifacts); @@ -140,9 +141,7 @@ describe('createRunnerFunction', () => { createRunnerFunction({ audits: mockAudits, targets: mockTargets, - })({ - outputDir: 'custom-output', - }), + })({ persist: DEFAULT_PERSIST_CONFIG }), ).resolves.toStrictEqual(mockedAuditOutputs); expect(loadArtifactsSpy).not.toHaveBeenCalled(); From 7a30721b8fe5e277cc6d88e1ef6d7933cf129dd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Chalk?= Date: Wed, 3 Sep 2025 17:21:41 +0200 Subject: [PATCH 2/3] feat(utils): convert runner args to and from environment variables --- packages/utils/src/index.ts | 2 + packages/utils/src/lib/env.ts | 42 +++++++++++++++ packages/utils/src/lib/env.unit.test.ts | 70 ++++++++++++++++++++++++- 3 files changed, 113 insertions(+), 1 deletion(-) diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 2ae826a06..099273815 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -16,6 +16,8 @@ export { isCI, isEnvVarEnabled, isVerbose, + runnerArgsFromEnv, + runnerArgsToEnv, } from './lib/env.js'; export { stringifyError } from './lib/errors.js'; export { diff --git a/packages/utils/src/lib/env.ts b/packages/utils/src/lib/env.ts index b77f17558..453d0f7ef 100644 --- a/packages/utils/src/lib/env.ts +++ b/packages/utils/src/lib/env.ts @@ -1,3 +1,11 @@ +import { + DEFAULT_PERSIST_FILENAME, + DEFAULT_PERSIST_FORMAT, + DEFAULT_PERSIST_OUTPUT_DIR, + DEFAULT_PERSIST_SKIP_REPORT, + type RunnerArgs, + formatSchema, +} from '@code-pushup/models'; import { ui } from './logging.js'; export function isCI() { @@ -51,3 +59,37 @@ export function coerceBooleanValue(value: unknown): boolean | undefined { return undefined; } + +type RUNNER_ARGS_ENV_VAR = + | 'CP_PERSIST_OUTPUT_DIR' + | 'CP_PERSIST_FILENAME' + | 'CP_PERSIST_FORMAT' + | 'CP_PERSIST_SKIP_REPORTS'; + +type RunnerEnv = Record; + +const FORMAT_SEP = ','; + +export function runnerArgsToEnv(config: RunnerArgs): RunnerEnv { + return { + CP_PERSIST_OUTPUT_DIR: config.persist.outputDir, + CP_PERSIST_FILENAME: config.persist.filename, + CP_PERSIST_FORMAT: config.persist.format.join(FORMAT_SEP), + CP_PERSIST_SKIP_REPORTS: config.persist.skipReports.toString(), + }; +} + +export function runnerArgsFromEnv(env: Partial): RunnerArgs { + const formats = env.CP_PERSIST_FORMAT?.split(FORMAT_SEP) + .map(item => formatSchema.safeParse(item).data) + .filter(item => item != null); + const skipReports = coerceBooleanValue(env.CP_PERSIST_SKIP_REPORTS); + return { + persist: { + outputDir: env.CP_PERSIST_OUTPUT_DIR || DEFAULT_PERSIST_OUTPUT_DIR, + filename: env.CP_PERSIST_FILENAME || DEFAULT_PERSIST_FILENAME, + format: formats?.length ? formats : DEFAULT_PERSIST_FORMAT, + skipReports: skipReports ?? DEFAULT_PERSIST_SKIP_REPORT, + }, + }; +} diff --git a/packages/utils/src/lib/env.unit.test.ts b/packages/utils/src/lib/env.unit.test.ts index 1ff0f92b5..ac125a5f1 100644 --- a/packages/utils/src/lib/env.unit.test.ts +++ b/packages/utils/src/lib/env.unit.test.ts @@ -1,4 +1,14 @@ -import { coerceBooleanValue, isEnvVarEnabled } from './env.js'; +import { + DEFAULT_PERSIST_FILENAME, + DEFAULT_PERSIST_FORMAT, + DEFAULT_PERSIST_SKIP_REPORT, +} from '@code-pushup/models'; +import { + coerceBooleanValue, + isEnvVarEnabled, + runnerArgsFromEnv, + runnerArgsToEnv, +} from './env.js'; import { ui } from './logging.js'; describe('isEnvVarEnabled', () => { @@ -65,3 +75,61 @@ describe('coerceBooleanValue', () => { expect(coerceBooleanValue(input)).toBe(expected); }); }); + +describe('runnerArgsToEnv', () => { + it('should convert runner args object to namespaced environment variables', () => { + expect( + runnerArgsToEnv({ + persist: { + outputDir: '.code-pushup', + filename: 'report', + format: ['json', 'md'], + skipReports: false, + }, + }), + ).toEqual({ + CP_PERSIST_OUTPUT_DIR: '.code-pushup', + CP_PERSIST_FILENAME: 'report', + CP_PERSIST_FORMAT: 'json,md', + CP_PERSIST_SKIP_REPORTS: 'false', + }); + }); +}); + +describe('runnerArgsFromEnv', () => { + it('should parse environment variables and create runner args object', () => { + expect( + runnerArgsFromEnv({ + CP_PERSIST_OUTPUT_DIR: '.code-pushup', + CP_PERSIST_FILENAME: 'report', + CP_PERSIST_FORMAT: 'json,md', + CP_PERSIST_SKIP_REPORTS: 'false', + }), + ).toEqual({ + persist: { + outputDir: '.code-pushup', + filename: 'report', + format: ['json', 'md'], + skipReports: false, + }, + }); + }); + + it('should fallback to defaults instead of empty or invalid values', () => { + expect( + runnerArgsFromEnv({ + CP_PERSIST_OUTPUT_DIR: '.code-pushup', + CP_PERSIST_FILENAME: '', + CP_PERSIST_FORMAT: 'html', + CP_PERSIST_SKIP_REPORTS: 'yup', + }), + ).toEqual({ + persist: { + outputDir: '.code-pushup', + filename: DEFAULT_PERSIST_FILENAME, + format: DEFAULT_PERSIST_FORMAT, + skipReports: DEFAULT_PERSIST_SKIP_REPORT, + }, + }); + }); +}); From 0c9fa377057936ee59cc1949975eaf4bb2030a1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Chalk?= Date: Wed, 3 Sep 2025 17:41:47 +0200 Subject: [PATCH 3/3] fix(core): use env vars instead of cli args for runner config --- .../core/src/lib/implementation/runner.ts | 5 +- .../lib/implementation/runner.unit.test.ts | 128 ++++++++++++------ 2 files changed, 88 insertions(+), 45 deletions(-) diff --git a/packages/core/src/lib/implementation/runner.ts b/packages/core/src/lib/implementation/runner.ts index 6d8d5989a..0db6becd0 100644 --- a/packages/core/src/lib/implementation/runner.ts +++ b/packages/core/src/lib/implementation/runner.ts @@ -15,9 +15,9 @@ import { executeProcess, fileExists, isVerbose, - objectToCliArgs, readJsonFile, removeDirectoryIfExists, + runnerArgsToEnv, ui, } from '@code-pushup/utils'; import { normalizeAuditOutputs } from '../normalize.js'; @@ -40,7 +40,7 @@ export async function executeRunnerConfig( const { duration, date } = await executeProcess({ command: config.command, - args: [...(config.args ?? []), ...objectToCliArgs(args)], + args: config.args, observer: { onStdout: stdout => { if (isVerbose()) { @@ -49,6 +49,7 @@ export async function executeRunnerConfig( }, onStderr: stderr => ui().logger.error(stderr), }, + env: { ...process.env, ...runnerArgsToEnv(args) }, }); // read process output from the file system and parse it diff --git a/packages/core/src/lib/implementation/runner.unit.test.ts b/packages/core/src/lib/implementation/runner.unit.test.ts index 06e962e3a..5f4bd8cf8 100644 --- a/packages/core/src/lib/implementation/runner.unit.test.ts +++ b/packages/core/src/lib/implementation/runner.unit.test.ts @@ -1,5 +1,13 @@ import { vol } from 'memfs'; -import { type AuditOutputs, auditOutputsSchema } from '@code-pushup/models'; +import { + type AuditOutputs, + DEFAULT_PERSIST_CONFIG, + DEFAULT_PERSIST_FILENAME, + DEFAULT_PERSIST_FORMAT, + DEFAULT_PERSIST_OUTPUT_DIR, + DEFAULT_PERSIST_SKIP_REPORT, + auditOutputsSchema, +} from '@code-pushup/models'; import { ISO_STRING_REGEXP, MEMFS_VOLUME, @@ -8,6 +16,7 @@ import { MINIMAL_RUNNER_FUNCTION_MOCK, osAgnosticPath, } from '@code-pushup/test-utils'; +import * as utils from '@code-pushup/utils'; import { type RunnerResult, executePluginRunner, @@ -38,10 +47,14 @@ describe('executeRunnerConfig', () => { }, MEMFS_VOLUME, ); + + vi.spyOn(utils, 'executeProcess'); }); it('should execute valid runner config', async () => { - const runnerResult = await executeRunnerConfig(MINIMAL_RUNNER_CONFIG_MOCK); + const runnerResult = await executeRunnerConfig(MINIMAL_RUNNER_CONFIG_MOCK, { + persist: DEFAULT_PERSIST_CONFIG, + }); // data sanity expect((runnerResult.audits as AuditOutputs)[0]?.slug).toBe('node-version'); @@ -50,23 +63,42 @@ describe('executeRunnerConfig', () => { // schema validation expect(() => auditOutputsSchema.parse(runnerResult.audits)).not.toThrow(); - }); - it('should use outputTransform when provided', async () => { - const runnerResult = await executeRunnerConfig({ + // executed process configuration + expect(utils.executeProcess).toHaveBeenCalledWith<[utils.ProcessConfig]>({ command: 'node', args: ['-v'], - outputFile: 'output.json', - outputTransform: (outputs: unknown): Promise => - Promise.resolve([ - { - slug: (outputs as AuditOutputs)[0]!.slug, - score: 0.3, - value: 16, - displayValue: '16.0.0', - }, - ]), + env: expect.objectContaining({ + CP_PERSIST_OUTPUT_DIR: DEFAULT_PERSIST_OUTPUT_DIR, + CP_PERSIST_FILENAME: DEFAULT_PERSIST_FILENAME, + CP_PERSIST_FORMAT: DEFAULT_PERSIST_FORMAT.join(','), + CP_PERSIST_SKIP_REPORTS: `${DEFAULT_PERSIST_SKIP_REPORT}`, + }), + observer: { + onStdout: expect.any(Function), + onStderr: expect.any(Function), + }, }); + }); + + it('should use outputTransform when provided', async () => { + const runnerResult = await executeRunnerConfig( + { + command: 'node', + args: ['-v'], + outputFile: 'output.json', + outputTransform: (outputs: unknown): Promise => + Promise.resolve([ + { + slug: (outputs as AuditOutputs)[0]!.slug, + score: 0.3, + value: 16, + displayValue: '16.0.0', + }, + ]), + }, + { persist: DEFAULT_PERSIST_CONFIG }, + ); const auditOutputs = runnerResult.audits as AuditOutputs; expect(auditOutputs[0]?.slug).toBe('node-version'); @@ -75,13 +107,16 @@ describe('executeRunnerConfig', () => { it('should throw if outputTransform throws', async () => { await expect( - executeRunnerConfig({ - command: 'node', - args: ['-v'], - outputFile: 'output.json', - outputTransform: () => - Promise.reject(new Error('Error: outputTransform has failed.')), - }), + executeRunnerConfig( + { + command: 'node', + args: ['-v'], + outputFile: 'output.json', + outputTransform: () => + Promise.reject(new Error('Error: outputTransform has failed.')), + }, + { persist: DEFAULT_PERSIST_CONFIG }, + ), ).rejects.toThrow('Error: outputTransform has failed.'); }); }); @@ -90,6 +125,7 @@ describe('executeRunnerFunction', () => { it('should execute a valid runner function', async () => { const runnerResult: RunnerResult = await executeRunnerFunction( MINIMAL_RUNNER_FUNCTION_MOCK, + { persist: DEFAULT_PERSIST_CONFIG }, ); const auditOutputs = runnerResult.audits as AuditOutputs; @@ -102,14 +138,12 @@ describe('executeRunnerFunction', () => { }); it('should throw if the runner function throws', async () => { - const nextSpy = vi.fn(); await expect( executeRunnerFunction( () => Promise.reject(new Error('Error: Runner has failed.')), - nextSpy, + { persist: DEFAULT_PERSIST_CONFIG }, ), ).rejects.toThrow('Error: Runner has failed.'); - expect(nextSpy).not.toHaveBeenCalled(); }); it('should throw with an invalid runner type', async () => { @@ -122,7 +156,9 @@ describe('executeRunnerFunction', () => { describe('executePluginRunner', () => { it('should execute a valid plugin config', async () => { - const pluginResult = await executePluginRunner(MINIMAL_PLUGIN_CONFIG_MOCK); + const pluginResult = await executePluginRunner(MINIMAL_PLUGIN_CONFIG_MOCK, { + persist: DEFAULT_PERSIST_CONFIG, + }); expect(pluginResult.audits[0]?.slug).toBe('node-version'); }); @@ -141,14 +177,17 @@ describe('executePluginRunner', () => { ); await expect( - executePluginRunner({ - ...MINIMAL_PLUGIN_CONFIG_MOCK, - runner: { - command: 'node', - args: ['-v'], - outputFile: 'output.json', + executePluginRunner( + { + ...MINIMAL_PLUGIN_CONFIG_MOCK, + runner: { + command: 'node', + args: ['-v'], + outputFile: 'output.json', + }, }, - }), + { persist: DEFAULT_PERSIST_CONFIG }, + ), ).resolves.toStrictEqual({ duration: expect.any(Number), date: expect.any(String), @@ -164,16 +203,19 @@ describe('executePluginRunner', () => { it('should yield audit outputs for a valid runner function', async () => { await expect( - executePluginRunner({ - ...MINIMAL_PLUGIN_CONFIG_MOCK, - runner: () => [ - { - slug: 'node-version', - score: 0.3, - value: 16, - }, - ], - }), + executePluginRunner( + { + ...MINIMAL_PLUGIN_CONFIG_MOCK, + runner: () => [ + { + slug: 'node-version', + score: 0.3, + value: 16, + }, + ], + }, + { persist: DEFAULT_PERSIST_CONFIG }, + ), ).resolves.toStrictEqual({ duration: expect.any(Number), date: expect.any(String),