diff --git a/packages/ci/README.md b/packages/ci/README.md index b570c39a2..ff2d83b66 100644 --- a/packages/ci/README.md +++ b/packages/ci/README.md @@ -64,10 +64,10 @@ If only the `head` is supplied, then Code PushUp will collect a new report and o If triggered by a pull request, then specify the `base` ref as well. This will additionally compare reports from both source and target branches and post a comment to the PR. -| Property | Required | Type | Description | -| :------- | :------: | :----------------------------- | :-------------------- | -| `head` | yes | `{ ref: string, sha: string }` | Current branch/commit | -| `base` | no | `{ ref: string, sha: string }` | Branch targeted by PR | +| Property | Required | Type | Description | +| :------- | :------: | :--------------------------------------- | :-------------------- | +| `head` | yes | `string \| { ref: string, sha: string }` | Current branch/commit | +| `base` | no | `string \| { ref: string, sha: string }` | Branch targeted by PR | ### Provider API client diff --git a/packages/ci/src/lib/git.ts b/packages/ci/src/lib/git.ts index 8f6978799..b30e62fda 100644 --- a/packages/ci/src/lib/git.ts +++ b/packages/ci/src/lib/git.ts @@ -1,4 +1,5 @@ -import { DiffNameStatus, simpleGit } from 'simple-git'; +import { DiffNameStatus, GitError, simpleGit } from 'simple-git'; +import type { GitBranch } from './models.js'; export type ChangedFiles = Record; @@ -12,6 +13,29 @@ type LineChange = { curr: { line: number; count: number }; }; +export async function normalizeGitRef( + ref: string | GitBranch, + git = simpleGit(), +): Promise { + if (typeof ref === 'object') { + return ref; + } + try { + const sha = await git.revparse(ref); + return { ref, sha }; + } catch (error) { + if ( + error instanceof GitError && + error.message.includes(`fatal: ambiguous argument '${ref}'`) + ) { + await git.fetch(['origin', ref, '--depth=1']); + const sha = await git.revparse('FETCH_HEAD'); + return { ref, sha }; + } + throw error; + } +} + export async function listChangedFiles( refs: { base: string; diff --git a/packages/ci/src/lib/models.ts b/packages/ci/src/lib/models.ts index 3c91e43a9..0a0309c8f 100644 --- a/packages/ci/src/lib/models.ts +++ b/packages/ci/src/lib/models.ts @@ -28,11 +28,11 @@ export type Options = { export type Settings = Required; /** - * Branches/commits for {@link runInCI} + * Branches/tags for {@link runInCI} */ export type GitRefs = { - head: GitBranch; - base?: GitBranch; + head: string | GitBranch; + base?: string | GitBranch; }; /** diff --git a/packages/ci/src/lib/run-monorepo.ts b/packages/ci/src/lib/run-monorepo.ts index 3bac54c08..cd93d3485 100644 --- a/packages/ci/src/lib/run-monorepo.ts +++ b/packages/ci/src/lib/run-monorepo.ts @@ -33,7 +33,6 @@ import { type RunEnv, checkPrintConfig, compareReports, - ensureHeadBranch, loadCachedBaseReport, printPersistConfig, runInBaseBranch, @@ -48,8 +47,6 @@ export async function runInMonorepoMode( logger.info('Running Code PushUp in monorepo mode'); - await ensureHeadBranch(env); - const { projects, runManyCommand } = await listMonorepoProjects(settings); const projectResults = runManyCommand ? await runProjectsInBulk(projects, runManyCommand, env) diff --git a/packages/ci/src/lib/run-standalone.ts b/packages/ci/src/lib/run-standalone.ts index 2f06497f3..f42e4ad19 100644 --- a/packages/ci/src/lib/run-standalone.ts +++ b/packages/ci/src/lib/run-standalone.ts @@ -1,6 +1,6 @@ import { commentOnPR } from './comment.js'; import type { StandaloneRunResult } from './models.js'; -import { type RunEnv, ensureHeadBranch, runOnProject } from './run-utils.js'; +import { type RunEnv, runOnProject } from './run-utils.js'; export async function runInStandaloneMode( env: RunEnv, @@ -10,8 +10,6 @@ export async function runInStandaloneMode( logger.info('Running Code PushUp in standalone project mode'); - await ensureHeadBranch(env); - const { files, newIssues } = await runOnProject(null, env); const commentMdPath = files.diff?.md; diff --git a/packages/ci/src/lib/run-utils.ts b/packages/ci/src/lib/run-utils.ts index 9947c963a..c3a45cf81 100644 --- a/packages/ci/src/lib/run-utils.ts +++ b/packages/ci/src/lib/run-utils.ts @@ -2,7 +2,10 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises'; import path from 'node:path'; import type { SimpleGit } from 'simple-git'; import type { CoreConfig, Report, ReportsDiff } from '@code-pushup/models'; -import { stringifyError } from '@code-pushup/utils'; +import { + removeUndefinedAndEmptyProps, + stringifyError, +} from '@code-pushup/utils'; import { type CommandContext, createCommandContext, @@ -12,12 +15,14 @@ import { runPrintConfig, } from './cli/index.js'; import { parsePersistConfig } from './cli/persist.js'; -import { listChangedFiles } from './git.js'; +import { DEFAULT_SETTINGS } from './constants.js'; +import { listChangedFiles, normalizeGitRef } from './git.js'; import { type SourceFileIssue, filterRelevantIssues } from './issues.js'; import type { GitBranch, GitRefs, Logger, + Options, OutputFiles, ProjectRunResult, ProviderAPIClient, @@ -26,12 +31,17 @@ import type { import type { ProjectConfig } from './monorepo/index.js'; export type RunEnv = { - refs: GitRefs; + refs: NormalizedGitRefs; api: ProviderAPIClient; settings: Settings; git: SimpleGit; }; +type NormalizedGitRefs = { + head: GitBranch; + base?: GitBranch; +}; + export type CompareReportsArgs = { project: ProjectConfig | null; env: RunEnv; @@ -53,6 +63,28 @@ export type BaseReportArgs = { ctx: CommandContext; }; +export async function createRunEnv( + refs: GitRefs, + api: ProviderAPIClient, + options: Options | undefined, + git: SimpleGit, +): Promise { + const [head, base] = await Promise.all([ + normalizeGitRef(refs.head, git), + refs.base && normalizeGitRef(refs.base, git), + ]); + + return { + refs: { head, ...(base && { base }) }, + api, + settings: { + ...DEFAULT_SETTINGS, + ...(options && removeUndefinedAndEmptyProps(options)), + }, + git, + }; +} + export async function runOnProject( project: ProjectConfig | null, env: RunEnv, @@ -81,9 +113,7 @@ export async function runOnProject( const noDiffOutput = { name: project?.name ?? '-', - files: { - report: reportFiles, - }, + files: { report: reportFiles }, } satisfies ProjectRunResult; if (base == null) { @@ -223,13 +253,6 @@ export async function loadCachedBaseReport( return null; } -export async function ensureHeadBranch({ refs, git }: RunEnv): Promise { - const { head } = refs; - if (head.sha !== (await git.revparse('HEAD'))) { - await git.checkout(['-f', head.ref]); - } -} - export async function runInBaseBranch( base: GitBranch, env: RunEnv, diff --git a/packages/ci/src/lib/run.ts b/packages/ci/src/lib/run.ts index 49473a1db..8eca6e228 100644 --- a/packages/ci/src/lib/run.ts +++ b/packages/ci/src/lib/run.ts @@ -1,15 +1,13 @@ import { type SimpleGit, simpleGit } from 'simple-git'; -import { DEFAULT_SETTINGS } from './constants.js'; import type { GitRefs, Options, ProviderAPIClient, RunResult, - Settings, } from './models.js'; import { runInMonorepoMode } from './run-monorepo.js'; import { runInStandaloneMode } from './run-standalone.js'; -import type { RunEnv } from './run-utils.js'; +import { createRunEnv } from './run-utils.js'; /** * Runs Code PushUp in CI environment. @@ -25,14 +23,9 @@ export async function runInCI( options?: Options, git: SimpleGit = simpleGit(), ): Promise { - const settings: Settings = { - ...DEFAULT_SETTINGS, - ...options, - }; + const env = await createRunEnv(refs, api, options, git); - const env: RunEnv = { refs, api, settings, git }; - - if (settings.monorepo) { + if (env.settings.monorepo) { return runInMonorepoMode(env); } diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 3d037a3e4..9ae059e14 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -114,6 +114,7 @@ export { objectToCliArgs, objectToEntries, objectToKeys, + removeUndefinedAndEmptyProps, toArray, toJsonLines, toNumberPrecision, diff --git a/packages/utils/src/lib/transform.ts b/packages/utils/src/lib/transform.ts index 4f6cc3802..f33aaa40e 100644 --- a/packages/utils/src/lib/transform.ts +++ b/packages/utils/src/lib/transform.ts @@ -151,3 +151,11 @@ export function toOrdinal(value: number): string { return `${value}th`; } + +export function removeUndefinedAndEmptyProps(obj: T): T { + return Object.fromEntries( + Object.entries(obj).filter( + ([, value]) => value !== undefined && value !== '', + ), + ) as T; +} diff --git a/packages/utils/src/lib/transform.unit.test.ts b/packages/utils/src/lib/transform.unit.test.ts index 6716d1e83..d4ad2cfcf 100644 --- a/packages/utils/src/lib/transform.unit.test.ts +++ b/packages/utils/src/lib/transform.unit.test.ts @@ -9,6 +9,7 @@ import { objectToCliArgs, objectToEntries, objectToKeys, + removeUndefinedAndEmptyProps, toArray, toJsonLines, toNumberPrecision, @@ -303,3 +304,16 @@ describe('toOrdinal', () => { expect(toOrdinal(value)).toBe(ordinalValue); }); }); + +describe('removeUndefinedAndEmptyProps', () => { + it('should omit empty strings and undefined', () => { + expect( + removeUndefinedAndEmptyProps({ foo: '', bar: undefined }), + ).toStrictEqual({}); + }); + + it('should preserve other values', () => { + const obj = { a: 'hello', b: 42, c: [], d: {}, e: null }; + expect(removeUndefinedAndEmptyProps(obj)).toStrictEqual(obj); + }); +});