diff --git a/plugins/circleci/src/circleci-config.ts b/plugins/circleci/src/circleci-config.ts index 3a8a0bdff..dad4af6b5 100644 --- a/plugins/circleci/src/circleci-config.ts +++ b/plugins/circleci/src/circleci-config.ts @@ -1,7 +1,8 @@ import type { CircleCiOptions, CircleCiSchema, - CircleCiWorkflow + CircleCiWorkflow, + CircleCiWorkflowJob } from '@dotcom-tool-kit/schemas/lib/hooks/circleci' import { type Conflict, isConflict } from '@dotcom-tool-kit/conflict' import { Hook, type HookInstallation } from '@dotcom-tool-kit/base' @@ -226,6 +227,27 @@ const customOptionsOverlap = ( }) } +const areDefinedAndUnequal = (a: T | undefined, b: T | undefined): boolean => + a !== undefined && b !== undefined && a !== b + +const workflowJobOptionsOverlap = ( + installation?: CircleCiWorkflowJob[], + other?: CircleCiWorkflowJob[] +): boolean => { + if (!installation || !other) { + return false + } + return installation.some((installationWorkflowJob) => { + const otherWorkflowJob = other.find(({ name }) => installationWorkflowJob.name === name) + return ( + otherWorkflowJob && + (areDefinedAndUnequal(installationWorkflowJob.runOnRelease, otherWorkflowJob.runOnRelease) || + areDefinedAndUnequal(installationWorkflowJob.splitIntoMatrix, otherWorkflowJob.splitIntoMatrix) || + customOptionsOverlap(installationWorkflowJob.custom, otherWorkflowJob.custom)) + ) + }) +} + const workflowOptionsOverlap = (installation?: CircleCiWorkflow[], other?: CircleCiWorkflow[]): boolean => { if (!installation || !other) { return false @@ -234,11 +256,9 @@ const workflowOptionsOverlap = (installation?: CircleCiWorkflow[], other?: Circl const otherWorkflow = other.find(({ name }) => installationWorkflow.name === name) return ( otherWorkflow && - ((installationWorkflow.runOnRelease !== undefined && - otherWorkflow.runOnRelease !== undefined && - installationWorkflow.runOnRelease !== otherWorkflow.runOnRelease) || + (areDefinedAndUnequal(installationWorkflow.runOnRelease, otherWorkflow.runOnRelease) || customOptionsOverlap(installationWorkflow.custom, otherWorkflow.custom) || - rootOptionOverlaps(installationWorkflow.jobs, otherWorkflow.jobs)) + workflowJobOptionsOverlap(installationWorkflow.jobs, otherWorkflow.jobs)) ) }) } @@ -290,7 +310,15 @@ const mergeInstallations = (installations: HookInstallation[]): const rootOptions = installations.flatMap<{ name: string }>( (installation) => installation.options[rootKey] ?? [] ) - return [rootKey, mergeRootOptions(rootOptions)] + const mergedOptions = mergeRootOptions(rootOptions) + if (rootKey === 'workflows') { + mergedOptions.forEach((workflow: CircleCiWorkflow) => { + if (workflow.jobs) { + workflow.jobs = mergeRootOptions(workflow.jobs) + } + }) + } + return [rootKey, mergedOptions] }) ), // squash all the custom options together diff --git a/plugins/circleci/test/circleci-config.test.ts b/plugins/circleci/test/circleci-config.test.ts index 882fcf0be..c9b0524d6 100644 --- a/plugins/circleci/test/circleci-config.test.ts +++ b/plugins/circleci/test/circleci-config.test.ts @@ -412,5 +412,50 @@ describe('CircleCI config hook', () => { } ]) }) + + it('should merge sibling plugins with different custom fields for a workflow job', () => { + const childInstallations: HookInstallation[] = [ + { + plugin: { id: 'a', root: 'plugins/a' }, + forHook: 'CircleCi', + hookConstructor: CircleCi, + options: { + jobs: [testJob], + workflows: [{ name: 'tool-kit', jobs: [{ ...testWorkflowJob, custom: { param1: 'a' } }] }] + } + }, + { + plugin: { id: 'b', root: 'plugins/b' }, + forHook: 'CircleCi', + hookConstructor: CircleCi, + options: { + workflows: [{ name: 'tool-kit', jobs: [{ name: testWorkflowJob.name, custom: { param2: 'b' } }] }] + } + } + ] + + const plugin = { id: 'p', root: 'plugins/p' } + + expect(CircleCi.mergeChildInstallations(plugin, childInstallations)).toEqual([ + { + plugin, + forHook: 'CircleCi', + hookConstructor: CircleCi, + options: expect.objectContaining({ + workflows: expect.arrayContaining([ + expect.objectContaining({ + name: 'tool-kit', + jobs: expect.arrayContaining([ + expect.objectContaining({ + name: testWorkflowJob.name, + custom: expect.objectContaining({ param1: 'a', param2: 'b' }) + }) + ]) + }) + ]) + }) + } + ]) + }) }) })