diff --git a/lib/config-validator.ts b/lib/config-validator.ts index 2436b8a8ab8182..2a8aff79a338da 100644 --- a/lib/config-validator.ts +++ b/lib/config-validator.ts @@ -17,6 +17,7 @@ import { let returnVal = 0; async function validate( + isGlobalConfig: boolean, desc: string, config: RenovateConfig, strict: boolean, @@ -36,7 +37,7 @@ async function validate( } } const massagedConfig = massageConfig(migratedConfig); - const res = await validateConfig(massagedConfig, isPreset); + const res = await validateConfig(isGlobalConfig, massagedConfig, isPreset); if (res.errors.length) { logger.error( { file: desc, errors: res.errors }, @@ -75,7 +76,7 @@ type PackageJson = { const parsedContent = await getParsedContent(file); try { logger.info(`Validating ${file}`); - await validate(file, parsedContent, strict); + await validate(true, file, parsedContent, strict); } catch (err) { logger.warn({ file, err }, 'File is not valid Renovate config'); returnVal = 1; @@ -96,7 +97,7 @@ type PackageJson = { const parsedContent = await getParsedContent(file); try { logger.info(`Validating ${file}`); - await validate(file, parsedContent, strict); + await validate(false, file, parsedContent, strict); } catch (err) { logger.warn({ file, err }, 'File is not valid Renovate config'); returnVal = 1; @@ -112,12 +113,18 @@ type PackageJson = { ) as PackageJson; if (pkgJson.renovate) { logger.info(`Validating package.json > renovate`); - await validate('package.json > renovate', pkgJson.renovate, strict); + await validate( + false, + 'package.json > renovate', + pkgJson.renovate, + strict, + ); } if (pkgJson['renovate-config']) { logger.info(`Validating package.json > renovate-config`); for (const presetConfig of Object.values(pkgJson['renovate-config'])) { await validate( + false, 'package.json > renovate-config', presetConfig, strict, @@ -134,7 +141,7 @@ type PackageJson = { const file = process.env.RENOVATE_CONFIG_FILE ?? 'config.js'; logger.info(`Validating ${file}`); try { - await validate(file, fileConfig, strict); + await validate(true, file, fileConfig, strict); } catch (err) { logger.error({ file, err }, 'File is not valid Renovate config'); returnVal = 1; diff --git a/lib/config/__snapshots__/validation.spec.ts.snap b/lib/config/__snapshots__/validation.spec.ts.snap index c102eace3c1a5b..14aef329c70218 100644 --- a/lib/config/__snapshots__/validation.spec.ts.snap +++ b/lib/config/__snapshots__/validation.spec.ts.snap @@ -37,6 +37,10 @@ exports[`config/validation validateConfig(config) catches invalid templates 1`] exports[`config/validation validateConfig(config) errors for all types 1`] = ` [ + { + "message": "Configuration option \`azureWorkItemId\` should be an integer. Found: false (boolean)", + "topic": "Configuration Error", + }, { "message": "Configuration option \`enabled\` should be boolean. Found: 1 (number)", "topic": "Configuration Error", @@ -57,10 +61,6 @@ exports[`config/validation validateConfig(config) errors for all types 1`] = ` "message": "Configuration option \`packageRules[3].matchPackagePatterns\` should be a list (Array)", "topic": "Configuration Error", }, - { - "message": "Configuration option \`prCommitsPerRunLimit\` should be an integer. Found: false (boolean)", - "topic": "Configuration Error", - }, { "message": "Configuration option \`semanticCommitType\` should be a string", "topic": "Configuration Error", diff --git a/lib/config/migrate-validate.ts b/lib/config/migrate-validate.ts index 81d46499852a47..a3d8f8b42c62a8 100644 --- a/lib/config/migrate-validate.ts +++ b/lib/config/migrate-validate.ts @@ -32,7 +32,7 @@ export async function migrateAndValidate( }: { warnings: ValidationMessage[]; errors: ValidationMessage[]; - } = await configValidation.validateConfig(massagedConfig); + } = await configValidation.validateConfig(false, massagedConfig); // istanbul ignore if if (is.nonEmptyArray(warnings)) { logger.warn({ warnings }, 'Found renovate config warnings'); diff --git a/lib/config/presets/internal/index.spec.ts b/lib/config/presets/internal/index.spec.ts index 88c27487f7c392..fd439b817579e8 100644 --- a/lib/config/presets/internal/index.spec.ts +++ b/lib/config/presets/internal/index.spec.ts @@ -30,7 +30,7 @@ describe('config/presets/internal/index', () => { const config = await resolveConfigPresets( massageConfig(presetConfig), ); - const res = await validateConfig(config, true); + const res = await validateConfig(false, config, true); expect(res.errors).toHaveLength(0); expect(res.warnings).toHaveLength(0); } catch (err) { diff --git a/lib/config/validation.spec.ts b/lib/config/validation.spec.ts index 744d0e46cbba1a..4290a28a148095 100644 --- a/lib/config/validation.spec.ts +++ b/lib/config/validation.spec.ts @@ -23,16 +23,48 @@ describe('config/validation', () => { const config = { prTitle: 'something', }; - const { warnings } = await configValidation.validateConfig(config); + const { warnings } = await configValidation.validateConfig(false, config); expect(warnings).toHaveLength(1); expect(warnings).toMatchSnapshot(); }); + it('catches global options in repo config', async () => { + const config = { + binarySource: 'something', + username: 'user', + }; + const { warnings } = await configValidation.validateConfig(false, config); + expect(warnings).toHaveLength(2); + expect(warnings).toMatchObject([ + { + message: `The "binarySource" option is a global option reserved only for bot's global configuration and cannot be configured within repository config file`, + }, + { + message: `The "username" option is a global option reserved only for bot's global configuration and cannot be configured within repository config file`, + }, + ]); + }); + + // false globals are the options which have names same to the another globalOnly option + it('does warn for false globals in repo config', async () => { + const config = { + hostRules: [ + { + username: 'user', + token: 'token', + password: 'pass', + }, + ], + }; + const { warnings } = await configValidation.validateConfig(false, config); + expect(warnings).toHaveLength(0); + }); + it('catches invalid templates', async () => { const config = { commitMessage: '{{{something}}', }; - const { errors } = await configValidation.validateConfig(config); + const { errors } = await configValidation.validateConfig(false, config); expect(errors).toHaveLength(1); expect(errors).toMatchSnapshot(); }); @@ -58,7 +90,7 @@ describe('config/validation', () => { }, ], }; - const { errors } = await configValidation.validateConfig(config); + const { errors } = await configValidation.validateConfig(false, config); expect(errors).toHaveLength(2); expect(errors).toMatchSnapshot(); }); @@ -83,7 +115,7 @@ describe('config/validation', () => { }, ], }; - const { errors } = await configValidation.validateConfig(config); + const { errors } = await configValidation.validateConfig(false, config); expect(errors).toHaveLength(2); }); @@ -112,7 +144,7 @@ describe('config/validation', () => { }, ], }; - const { errors } = await configValidation.validateConfig(config); + const { errors } = await configValidation.validateConfig(false, config); expect(errors).toHaveLength(2); expect(errors).toMatchSnapshot(); }); @@ -127,7 +159,7 @@ describe('config/validation', () => { }, }, } as any; - const { errors } = await configValidation.validateConfig(config); + const { errors } = await configValidation.validateConfig(false, config); expect(errors).toMatchObject([ { message: @@ -154,7 +186,7 @@ describe('config/validation', () => { }, }; // @ts-expect-error invalid options - const { errors } = await configValidation.validateConfig(config); + const { errors } = await configValidation.validateConfig(false, config); expect(errors).toMatchObject([ { message: @@ -174,7 +206,7 @@ describe('config/validation', () => { randomKey: '', }, } as any; - const { errors } = await configValidation.validateConfig(config); + const { errors } = await configValidation.validateConfig(false, config); expect(errors).toMatchObject([ { message: @@ -187,7 +219,7 @@ describe('config/validation', () => { const config = { baseBranches: ['/***$}{]][/'], }; - const { errors } = await configValidation.validateConfig(config); + const { errors } = await configValidation.validateConfig(false, config); expect(errors).toEqual([ { topic: 'Configuration Error', @@ -213,8 +245,10 @@ describe('config/validation', () => { }, major: null, }; - const { warnings, errors } = - await configValidation.validateConfig(config); + const { warnings, errors } = await configValidation.validateConfig( + false, + config, + ); expect(warnings).toHaveLength(0); expect(errors).toHaveLength(3); expect(errors).toMatchSnapshot(); @@ -229,8 +263,10 @@ describe('config/validation', () => { }, ], }; - const { warnings, errors } = - await configValidation.validateConfig(config); + const { warnings, errors } = await configValidation.validateConfig( + false, + config, + ); expect(warnings).toHaveLength(0); expect(errors).toHaveLength(1); expect(errors[0].message).toContain('ansible'); @@ -246,6 +282,7 @@ describe('config/validation', () => { ], }; const { warnings, errors } = await configValidation.validateConfig( + false, config as any, ); expect(warnings).toHaveLength(0); @@ -264,8 +301,10 @@ describe('config/validation', () => { }, ], ])('validates enabled managers for %s', async (_case, config) => { - const { warnings, errors } = - await configValidation.validateConfig(config); + const { warnings, errors } = await configValidation.validateConfig( + false, + config, + ); expect(warnings).toHaveLength(0); expect(errors).toHaveLength(0); }); @@ -280,8 +319,10 @@ describe('config/validation', () => { ])( 'errors if included not supported enabled managers for %s', async (_case, config) => { - const { warnings, errors } = - await configValidation.validateConfig(config); + const { warnings, errors } = await configValidation.validateConfig( + false, + config, + ); expect(warnings).toHaveLength(0); expect(errors).toHaveLength(1); expect(errors).toMatchSnapshot(); @@ -296,7 +337,7 @@ describe('config/validation', () => { schedule: ['every 15 mins every weekday'], timezone: 'Asia', labels: 5 as any, - prCommitsPerRunLimit: false as any, + azureWorkItemId: false as any, semanticCommitType: 7 as any, lockFileMaintenance: false as any, extends: [':timezone(Europe/Brussel)'], @@ -319,8 +360,10 @@ describe('config/validation', () => { ], major: null, }; - const { warnings, errors } = - await configValidation.validateConfig(config); + const { warnings, errors } = await configValidation.validateConfig( + false, + config, + ); expect(warnings).toHaveLength(1); expect(errors).toMatchSnapshot(); expect(errors).toHaveLength(15); @@ -346,8 +389,10 @@ describe('config/validation', () => { }, }, }; - const { warnings, errors } = - await configValidation.validateConfig(config); + const { warnings, errors } = await configValidation.validateConfig( + false, + config, + ); expect(warnings).toHaveLength(4); expect(errors).toMatchSnapshot(); expect(errors).toHaveLength(4); @@ -364,6 +409,7 @@ describe('config/validation', () => { ], }; const { warnings, errors } = await configValidation.validateConfig( + false, config, true, ); @@ -381,8 +427,10 @@ describe('config/validation', () => { fileMatch: ['x?+'], }, }; - const { warnings, errors } = - await configValidation.validateConfig(config); + const { warnings, errors } = await configValidation.validateConfig( + false, + config, + ); expect(warnings).toHaveLength(0); expect(errors).toHaveLength(2); expect(errors).toMatchSnapshot(); @@ -401,6 +449,7 @@ describe('config/validation', () => { ], }; const { warnings, errors } = await configValidation.validateConfig( + false, config, true, ); @@ -419,6 +468,7 @@ describe('config/validation', () => { ], }; const { warnings, errors } = await configValidation.validateConfig( + false, config as any, true, ); @@ -446,6 +496,7 @@ describe('config/validation', () => { ], }; const { warnings, errors } = await configValidation.validateConfig( + false, config as any, true, ); @@ -474,6 +525,7 @@ describe('config/validation', () => { ], }; const { warnings, errors } = await configValidation.validateConfig( + false, config as any, true, ); @@ -510,6 +562,7 @@ describe('config/validation', () => { ], }; const { warnings, errors } = await configValidation.validateConfig( + false, config as RenovateConfig, true, ); @@ -540,6 +593,7 @@ describe('config/validation', () => { ], }; const { warnings, errors } = await configValidation.validateConfig( + false, config as any, true, ); @@ -561,6 +615,7 @@ describe('config/validation', () => { ], }; const { warnings, errors } = await configValidation.validateConfig( + false, config, true, ); @@ -581,6 +636,7 @@ describe('config/validation', () => { ], }; const { warnings, errors } = await configValidation.validateConfig( + false, config, true, ); @@ -604,6 +660,7 @@ describe('config/validation', () => { ], }; const { warnings, errors } = await configValidation.validateConfig( + false, config, true, ); @@ -626,6 +683,7 @@ describe('config/validation', () => { ], }; const { warnings, errors } = await configValidation.validateConfig( + false, config as any, true, ); @@ -646,6 +704,7 @@ describe('config/validation', () => { ], }; const { warnings, errors } = await configValidation.validateConfig( + false, config, true, ); @@ -659,6 +718,7 @@ describe('config/validation', () => { $schema: 'renovate.json', }; const { warnings, errors } = await configValidation.validateConfig( + false, config, true, ); @@ -671,6 +731,7 @@ describe('config/validation', () => { extends: [':timezone', ':timezone(Europe/Berlin)'], }; const { warnings, errors } = await configValidation.validateConfig( + false, config, true, ); @@ -683,6 +744,7 @@ describe('config/validation', () => { constraints: { packageRules: [{}] }, }; const { warnings, errors } = await configValidation.validateConfig( + false, config as never, // TODO: #15963 true, ); @@ -695,6 +757,7 @@ describe('config/validation', () => { prBodyDefinitions: {}, }; const { warnings, errors } = await configValidation.validateConfig( + false, config, true, ); @@ -709,8 +772,10 @@ describe('config/validation', () => { example2: 'https://www.example2.com/example', }, }; - const { warnings, errors } = - await configValidation.validateConfig(config); + const { warnings, errors } = await configValidation.validateConfig( + false, + config, + ); expect(warnings).toHaveLength(0); expect(errors).toHaveLength(0); }); @@ -723,8 +788,10 @@ describe('config/validation', () => { } as unknown as string, // intentional incorrect config to check error message }, }; - const { warnings, errors } = - await configValidation.validateConfig(config); + const { warnings, errors } = await configValidation.validateConfig( + false, + config, + ); expect(warnings).toHaveLength(0); expect(errors).toMatchObject([ { @@ -742,8 +809,10 @@ describe('config/validation', () => { example2: 'http://www.example.com', }, }; - const { warnings, errors } = - await configValidation.validateConfig(config); + const { warnings, errors } = await configValidation.validateConfig( + false, + config, + ); expect(warnings).toHaveLength(0); expect(errors).toMatchObject([ { @@ -773,8 +842,10 @@ describe('config/validation', () => { }, ], }; - const { warnings, errors } = - await configValidation.validateConfig(config); + const { warnings, errors } = await configValidation.validateConfig( + false, + config, + ); expect(errors).toHaveLength(1); expect(warnings).toHaveLength(1); expect(errors).toMatchSnapshot(); @@ -792,8 +863,10 @@ describe('config/validation', () => { }, }, } as never; - const { warnings, errors } = - await configValidation.validateConfig(config); + const { warnings, errors } = await configValidation.validateConfig( + false, + config, + ); expect(errors).toHaveLength(1); expect(warnings).toHaveLength(0); expect(errors).toMatchSnapshot(); @@ -803,8 +876,10 @@ describe('config/validation', () => { const config = { hostType: 'npm', }; - const { warnings, errors } = - await configValidation.validateConfig(config); + const { warnings, errors } = await configValidation.validateConfig( + false, + config, + ); expect(errors).toHaveLength(0); expect(warnings).toHaveLength(1); expect(warnings).toMatchSnapshot(); @@ -815,6 +890,7 @@ describe('config/validation', () => { extends: ['foo', 'bar', 42] as never, }; const { warnings, errors } = await configValidation.validateConfig( + false, config, true, ); @@ -829,6 +905,7 @@ describe('config/validation', () => { ], }; const { warnings, errors } = await configValidation.validateConfig( + false, config, true, ); @@ -847,6 +924,7 @@ describe('config/validation', () => { ], } as any; const { warnings, errors } = await configValidation.validateConfig( + false, config, true, ); @@ -866,6 +944,7 @@ describe('config/validation', () => { ], }; const { warnings, errors } = await configValidation.validateConfig( + false, config, true, ); @@ -880,8 +959,10 @@ describe('config/validation', () => { example2: 'https://www.example2.com/example', }, }; - const { warnings, errors } = - await configValidation.validateConfig(config); + const { warnings, errors } = await configValidation.validateConfig( + true, + config, + ); expect(warnings).toHaveLength(0); expect(errors).toHaveLength(0); }); @@ -893,8 +974,10 @@ describe('config/validation', () => { example2: 123, }, }; - const { warnings, errors } = - await configValidation.validateConfig(config); + const { warnings, errors } = await configValidation.validateConfig( + true, + config, + ); expect(warnings).toHaveLength(0); expect(errors).toMatchObject([ { @@ -909,8 +992,10 @@ describe('config/validation', () => { const config = { schedule: ['30 5 * * *'], }; - const { warnings, errors } = - await configValidation.validateConfig(config); + const { warnings, errors } = await configValidation.validateConfig( + false, + config, + ); expect(warnings).toHaveLength(0); expect(errors).toMatchObject([ { diff --git a/lib/config/validation.ts b/lib/config/validation.ts index 34753754e0459c..acb3253ba4685c 100644 --- a/lib/config/validation.ts +++ b/lib/config/validation.ts @@ -30,6 +30,7 @@ const options = getOptions(); let optionTypes: Record; let optionParents: Record; +let optionGlobals: Set; const managerList = getManagerList(); @@ -98,6 +99,7 @@ export function getParentName(parentPath: string | undefined): string { } export async function validateConfig( + isGlobalConfig: boolean, config: RenovateConfig, isPreset?: boolean, parentPath?: string, @@ -116,6 +118,7 @@ export async function validateConfig( } }); } + let errors: ValidationMessage[] = []; let warnings: ValidationMessage[] = []; @@ -139,6 +142,24 @@ export async function validateConfig( message: `The "${key}" object can only be configured at the top level of a config but was found inside "${parentPath}"`, }); } + if (!isGlobalConfig) { + if (!optionGlobals) { + optionGlobals = new Set(); + for (const option of options) { + if (option.globalOnly) { + optionGlobals.add(option.name); + } + } + } + + if (optionGlobals.has(key) && !isFalseGlobal(key, parentPath)) { + warnings.push({ + topic: 'Configuration Error', + message: `The "${key}" option is a global option reserved only for bot's global configuration and cannot be configured within repository config file`, + }); + continue; + } + } if (key === 'enabledManagers' && val) { const unsupportedManagers = getUnsupportedEnabledManagers( val as string[], @@ -273,6 +294,7 @@ export async function validateConfig( for (const [subIndex, subval] of val.entries()) { if (is.object(subval)) { const subValidation = await validateConfig( + isGlobalConfig, subval as RenovateConfig, isPreset, `${currentPath}[${subIndex}]`, @@ -656,6 +678,7 @@ export async function validateConfig( .map((option) => option.name); if (!ignoredObjects.includes(key)) { const subValidation = await validateConfig( + isGlobalConfig, val, isPreset, currentPath, @@ -732,3 +755,22 @@ function validateRegexManagerFields( } } } + +/** An option is a false global if it has the same name as a global only option + * but is actually just the field of a non global option or field an children of the non global option + * eg. token: it's global option used as the bot's token as well and + * also it can be the token used for a platform inside the hostRules configuration + */ +function isFalseGlobal(optionName: string, parentPath?: string): boolean { + if (parentPath?.includes('hostRules')) { + if ( + optionName === 'token' || + optionName === 'username' || + optionName === 'password' + ) { + return true; + } + } + + return false; +} diff --git a/lib/workers/repository/reconfigure/index.ts b/lib/workers/repository/reconfigure/index.ts index 0908bbf82c5e83..22ee0135f9356f 100644 --- a/lib/workers/repository/reconfigure/index.ts +++ b/lib/workers/repository/reconfigure/index.ts @@ -151,7 +151,7 @@ export async function validateReconfigureBranch( } // perform validation and provide a passing or failing check run based on result - const validationResult = await validateConfig(configFileParsed); + const validationResult = await validateConfig(false, configFileParsed); // failing check if (validationResult.errors.length > 0) {