diff --git a/lib/modules/manager/circleci/extract.spec.ts b/lib/modules/manager/circleci/extract.spec.ts index 7feeb83add72139..25915c53639f842 100644 --- a/lib/modules/manager/circleci/extract.spec.ts +++ b/lib/modules/manager/circleci/extract.spec.ts @@ -249,5 +249,65 @@ describe('modules/manager/circleci/extract', () => { }, ]); }); + + it('extracts orb definitions', () => { + const res = extractPackageFile(codeBlock` + version: 2.1 + + orbs: + myorb: + orbs: + python: circleci/python@2.1.1 + + executors: + python: + docker: + - image: cimg/python:3.9 + + jobs: + test_image: + docker: + - image: cimg/python:3.7 + steps: + - checkout + + workflows: + Test: + jobs: + - myorb/test_image`); + + expect(res).toEqual({ + deps: [ + { + currentValue: '2.1.1', + datasource: 'orb', + depName: 'python', + depType: 'orb', + packageName: 'circleci/python', + versioning: 'npm', + }, + { + autoReplaceStringTemplate: + '{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}', + currentDigest: undefined, + currentValue: '3.9', + datasource: 'docker', + depName: 'cimg/python', + depType: 'docker', + replaceString: 'cimg/python:3.9', + }, + { + autoReplaceStringTemplate: + '{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}', + currentDigest: undefined, + currentValue: '3.7', + datasource: 'docker', + depName: 'cimg/python', + depType: 'docker', + replaceString: 'cimg/python:3.7', + }, + ], + }); + }); }); }); diff --git a/lib/modules/manager/circleci/extract.ts b/lib/modules/manager/circleci/extract.ts index 660fb4eb35e6208..ccdc5cb6404f2e8 100644 --- a/lib/modules/manager/circleci/extract.ts +++ b/lib/modules/manager/circleci/extract.ts @@ -9,20 +9,17 @@ import type { PackageDependency, PackageFileContent, } from '../types'; -import { CircleCiFile, type CircleCiJob } from './schema'; +import { CircleCiFile, type CircleCiJob, type CircleCiOrb } from './schema'; -export function extractPackageFile( - content: string, +function extractDefinition( + definition: CircleCiOrb | CircleCiFile, packageFile?: string, config?: ExtractConfig, -): PackageFileContent | null { +): PackageDependency[] { const deps: PackageDependency[] = []; - try { - const parsed = parseSingleYaml(content, { - customSchema: CircleCiFile, - }); - for (const [key, orb] of Object.entries(parsed.orbs ?? {})) { + for (const [key, orb] of Object.entries(definition.orbs ?? {})) { + if (typeof orb === 'string') { const [packageName, currentValue] = orb.split('@'); deps.push({ @@ -33,21 +30,40 @@ export function extractPackageFile( versioning: npmVersioning.id, datasource: OrbDatasource.id, }); + } else { + deps.push(...extractDefinition(orb)); } + } - // extract environments - const environments: CircleCiJob[] = [ - Object.values(parsed.executors ?? {}), - Object.values(parsed.jobs ?? {}), - ].flat(); - for (const job of environments) { - for (const dockerElement of coerceArray(job.docker)) { - deps.push({ - ...getDep(dockerElement.image, true, config?.registryAliases), - depType: 'docker', - }); - } + // extract environments + const environments: CircleCiJob[] = [ + Object.values(definition.executors ?? {}), + Object.values(definition.jobs ?? {}), + ].flat(); + for (const job of environments) { + for (const dockerElement of coerceArray(job.docker)) { + deps.push({ + ...getDep(dockerElement.image, true, config?.registryAliases), + depType: 'docker', + }); } + } + + return deps; +} + +export function extractPackageFile( + content: string, + packageFile?: string, + config?: ExtractConfig, +): PackageFileContent | null { + const deps: PackageDependency[] = []; + try { + const parsed = parseSingleYaml(content, { + customSchema: CircleCiFile, + }); + + deps.push(...extractDefinition(parsed, packageFile, config)); for (const alias of coerceArray(parsed.aliases)) { deps.push({ diff --git a/lib/modules/manager/circleci/schema.ts b/lib/modules/manager/circleci/schema.ts index 2aa96811019fc31..f4288727a2845f7 100644 --- a/lib/modules/manager/circleci/schema.ts +++ b/lib/modules/manager/circleci/schema.ts @@ -4,14 +4,31 @@ export const CircleCiDocker = z.object({ image: z.string(), }); -export type CircleCiJob = z.infer; export const CircleCiJob = z.object({ docker: z.array(CircleCiDocker).optional(), }); +export type CircleCiJob = z.infer; + +const baseOrb = z.object({ + executors: z.record(z.string(), CircleCiJob).optional(), + jobs: z.record(z.string(), CircleCiJob).optional(), +}); + +type Orb = z.infer & { + orbs?: Record; +}; + +export const CircleCiOrb: z.ZodType = baseOrb.extend({ + orbs: z.lazy(() => + z.record(z.string(), z.union([z.string(), CircleCiOrb])).optional(), + ), +}); +export type CircleCiOrb = z.infer; export const CircleCiFile = z.object({ aliases: z.array(CircleCiDocker).optional(), executors: z.record(z.string(), CircleCiJob).optional(), jobs: z.record(z.string(), CircleCiJob).optional(), - orbs: z.record(z.string()).optional(), + orbs: z.record(z.string(), z.union([z.string(), CircleCiOrb])).optional(), }); +export type CircleCiFile = z.infer;