diff --git a/lib/modules/versioning/index.ts b/lib/modules/versioning/index.ts index b26e22d9552ede..0346a213a622a6 100644 --- a/lib/modules/versioning/index.ts +++ b/lib/modules/versioning/index.ts @@ -1,13 +1,9 @@ -import { logger } from '../../logger'; import versionings from './api'; -import { isVersioningApiConstructor } from './common'; -import * as semverCoerced from './semver-coerced'; +import { Versioning } from './schema'; import type { VersioningApi, VersioningApiConstructor } from './types'; export * from './types'; -const defaultVersioning = semverCoerced; - export const getVersioningList = (): string[] => Array.from(versionings.keys()); /** * Get versioning map. Can be used to dynamically add new versioning type @@ -17,28 +13,18 @@ export const getVersionings = (): Map< VersioningApi | VersioningApiConstructor > => versionings; -export function get(versioning: string | undefined): VersioningApi { - if (!versioning) { - logger.trace( - `Missing versioning, using ${defaultVersioning.id} as fallback.` - ); - return defaultVersioning.api; - } - const [versioningName, ...versioningRest] = versioning.split(':'); - const versioningConfig = versioningRest.length - ? versioningRest.join(':') - : undefined; +export function get(versioning = ''): VersioningApi { + const res = Versioning.safeParse(versioning); - const theVersioning = versionings.get(versioningName); - if (!theVersioning) { - logger.info( - { versioning }, - `Unknown versioning - defaulting to ${defaultVersioning.id}` - ); - return defaultVersioning.api; - } - if (isVersioningApiConstructor(theVersioning)) { - return new theVersioning(versioningConfig); + if (!res.success) { + const [issue] = res.error.issues; + if (issue && issue.code === 'custom' && issue.params?.error) { + throw issue.params.error; + } + + // istanbul ignore next: should never happen + throw res.error; } - return theVersioning; + + return res.data; } diff --git a/lib/modules/versioning/regex/index.spec.ts b/lib/modules/versioning/regex/index.spec.ts index a97494482a15c8..3c96498aa81195 100644 --- a/lib/modules/versioning/regex/index.spec.ts +++ b/lib/modules/versioning/regex/index.spec.ts @@ -22,15 +22,14 @@ describe('modules/versioning/regex/index', () => { }); describe('throws', () => { - for (const re of [ - '^(?\\d+)(', - '^(?\\d+)?(?\\d+)?(?<=y)x$', - ]) { - it(re, () => { - expect(() => get(`regex:${re}`)).toThrow(CONFIG_VALIDATION); - }); - } + it.each` + regex + ${'^(?\\d+)('} + ${'^(?\\d+)?(?\\d+)?(?<=y)x$'} + `(`on invalid regex: "$regex"`, ({ re }: { re: string }) => { + expect(() => get(`regex:${re}`)).toThrow(CONFIG_VALIDATION); + }); }); it.each` diff --git a/lib/modules/versioning/schema.spec.ts b/lib/modules/versioning/schema.spec.ts new file mode 100644 index 00000000000000..4e31686fafe1ce --- /dev/null +++ b/lib/modules/versioning/schema.spec.ts @@ -0,0 +1,23 @@ +import api from './api'; +import { Versioning } from './schema'; + +describe('modules/versioning/schema', () => { + it('returns existing version scheme', () => { + const versioning1 = Versioning.parse('hermit'); + const versioning2 = Versioning.parse('hermit:foobar'); + expect(versioning1.isValid).toBeFunction(); + expect(versioning2.isValid).toBeFunction(); + expect(versioning1).not.toBe(versioning2); + }); + + it('falls back to default version scheme', () => { + const defaultVersioning = api.get('semver-coerced'); + expect(Versioning.parse('foobarbaz')).toBe(defaultVersioning); + expect(Versioning.parse('')).toBe(defaultVersioning); + }); + + it('catches errors', () => { + const res = Versioning.safeParse('regex:foobar'); + expect(res.success).toBeFalse(); + }); +}); diff --git a/lib/modules/versioning/schema.ts b/lib/modules/versioning/schema.ts new file mode 100644 index 00000000000000..3471ecbed5172d --- /dev/null +++ b/lib/modules/versioning/schema.ts @@ -0,0 +1,40 @@ +import is from '@sindresorhus/is'; +import { z } from 'zod'; +import { logger } from '../../logger'; +import versionings from './api'; +import * as defaultVersioning from './semver-coerced'; +import type { VersioningApi } from './types'; + +export const Versioning = z + .string() + .transform((versioningSpec, ctx): VersioningApi => { + const [versioningName, ...versioningRest] = versioningSpec.split(':'); + + let versioning = versionings.get(versioningName); + if (!versioning) { + logger.info( + { versioning: versioningSpec }, + `Versioning: '${versioningSpec}' not found, falling back to ${defaultVersioning.id}` + ); + return defaultVersioning.api; + } + + if (is.function_(versioning)) { + const versioningConfig = versioningRest.length + ? versioningRest.join(':') + : undefined; + + try { + versioning = new versioning(versioningConfig); + } catch (error) { + ctx.addIssue({ + code: 'custom', + message: `Versioning: '${versioningSpec}' failed to initialize`, + params: { error }, + }); + return z.NEVER; + } + } + + return versioning; + });