From c39850a4df4884bd3141288b3c2bf583c276a65c Mon Sep 17 00:00:00 2001 From: Ivo Murrell Date: Wed, 27 Nov 2024 14:43:50 +0000 Subject: [PATCH] feat(circleci): allow plugins to only specify a subset of hook options This is so that one plugin can override the options of a parent plugin. In the future it might be worth validating that the final merged hook options include all the required options but that will be slightly more complicated and I don't think it's necessary right now. --- lib/schemas/src/hooks/circleci.ts | 108 +++++++++++++----------- plugins/circleci/src/circleci-config.ts | 8 +- 2 files changed, 65 insertions(+), 51 deletions(-) diff --git a/lib/schemas/src/hooks/circleci.ts b/lib/schemas/src/hooks/circleci.ts index f40e91988..bd0aad65d 100644 --- a/lib/schemas/src/hooks/circleci.ts +++ b/lib/schemas/src/hooks/circleci.ts @@ -3,65 +3,79 @@ import { z } from 'zod' export const CircleCiCustom = z.record(z.unknown()) export type CircleCiCustom = z.infer -export const CircleCiExecutor = z.object({ - name: z.string(), - image: z.string() -}) +export const CircleCiExecutor = z + .object({ + name: z.string(), + image: z.string() + }) + .partial() + .required({ name: true }) export type CircleCiExecutor = z.infer -export const CircleCiJob = z.object({ - name: z.string(), - command: z.string() -}) +export const CircleCiJob = z + .object({ + name: z.string(), + command: z.string() + }) + .partial() + .required({ name: true }) export type CircleCiJob = z.infer -export const CircleCiWorkflowJob = z.object({ - name: z.string(), - requires: z.array(z.string()), - splitIntoMatrix: z.boolean().optional(), - runOnRelease: z.boolean().default(true), - custom: CircleCiCustom.optional() -}) +export const CircleCiWorkflowJob = z + .object({ + name: z.string(), + requires: z.array(z.string()), + splitIntoMatrix: z.boolean().optional(), + runOnRelease: z.boolean().default(true), + custom: CircleCiCustom.optional() + }) + .partial() + .required({ name: true }) export type CircleCiWorkflowJob = z.infer -export const CircleCiWorkflow = z.object({ - name: z.string(), - jobs: z.array(CircleCiWorkflowJob), - runOnRelease: z.boolean().optional(), - custom: CircleCiCustom.optional() -}) +export const CircleCiWorkflow = z + .object({ + name: z.string(), + jobs: z.array(CircleCiWorkflowJob), + runOnRelease: z.boolean().optional(), + custom: CircleCiCustom.optional() + }) + .partial() + .required({ name: true }) export type CircleCiWorkflow = z.infer export const CircleCiCustomConfig = CircleCiCustom export type CircleCiCustomConfig = z.infer -export const CircleCiSchema = z.object({ - executors: z - .array(CircleCiExecutor) - .optional() - .describe('an array of additional CircleCI executors to output in the generated config.'), - jobs: z - .array(CircleCiJob) - .optional() - .describe( - 'an array of additional CircleCI jobs to output in the generated config. these are used for running Tool Kit commands. for running arbitrary shell commands, use `custom`.' +export const CircleCiSchema = z + .object({ + executors: z + .array(CircleCiExecutor) + .optional() + .describe('an array of additional CircleCI executors to output in the generated config.'), + jobs: z + .array(CircleCiJob) + .optional() + .describe( + 'an array of additional CircleCI jobs to output in the generated config. these are used for running Tool Kit commands. for running arbitrary shell commands, use `custom`.' + ), + workflows: z + .array(CircleCiWorkflow) + .optional() + .describe( + 'an array of additional CircleCI workflows to output in the generated config. these reference jobs defined in the `jobs` option.' + ), + custom: CircleCiCustomConfig.optional().describe( + 'arbitrary additional CircleCI configuration that will be merged into the Tool Kit-generated config.' ), - workflows: z - .array(CircleCiWorkflow) - .optional() - .describe( - 'an array of additional CircleCI workflows to output in the generated config. these reference jobs defined in the `jobs` option.' - ), - custom: CircleCiCustomConfig.optional().describe( - 'arbitrary additional CircleCI configuration that will be merged into the Tool Kit-generated config.' - ), - disableBaseConfig: z - .boolean() - .optional() - .describe( - 'set to `true` to omit the Tool Kit CircleCI boilerplate. should be used along with `custom` to provide your own boilerplate.' - ) -}) + disableBaseConfig: z + .boolean() + .optional() + .describe( + 'set to `true` to omit the Tool Kit CircleCI boilerplate. should be used along with `custom` to provide your own boilerplate.' + ) + }) + .partial() .describe(`This hook automatically manages \`.circleci/config.yml\` in your repo to provide configuration for CircleCI workflows to run Tool Kit commands and tasks. Options provided in your repository's \`.toolkitrc.yml\` for this hook are merged with any Tool Kit plugin that also provides options for the hook. diff --git a/plugins/circleci/src/circleci-config.ts b/plugins/circleci/src/circleci-config.ts index d9211681e..3a8a0bdff 100644 --- a/plugins/circleci/src/circleci-config.ts +++ b/plugins/circleci/src/circleci-config.ts @@ -74,7 +74,7 @@ export type CircleCIState = CircleConfig * hook's config into the larger state, and so each hook only needs to define * the parts of the config they want to add to. */ -export type CircleCIStatePartial = PartialDeep +export type CircleCIStatePartial = PartialDeep // Make this function lazy so that the global options object will have been // populated first. @@ -325,9 +325,9 @@ const mergeInstallationResults = ( const toolKitOrbPrefix = (job: string) => `tool-kit/${job}` -const generateJobs = (workflow: CircleCiWorkflow): Job[] => { +const generateJobs = (workflow: CircleCiWorkflow): Job[] | undefined => { const runsOnMultipleNodeVersions = getNodeVersions().length > 1 - return workflow.jobs.map((job) => { + return workflow.jobs?.map((job) => { const splitIntoMatrix = runsOnMultipleNodeVersions && (job.splitIntoMatrix ?? true) return { [toolKitOrbPrefix(job.name)]: merge( @@ -337,7 +337,7 @@ const generateJobs = (workflow: CircleCiWorkflow): Job[] => { executor: 'node' }, { - requires: job.requires.map((required) => { + requires: job.requires?.map((required) => { if (required === 'checkout') { return required }