From bc80c8c75c29604439e3c4868a07f44b7bb9a4db Mon Sep 17 00:00:00 2001 From: Rhys Arkins Date: Sat, 13 Apr 2024 07:20:08 +0200 Subject: [PATCH 1/3] feat(config): validate top-level options --- lib/config/defaults.ts | 3 ++- lib/config/options/index.ts | 26 ++++++++++++++++++++++++++ lib/config/types.ts | 1 + lib/config/validation.ts | 6 +++++- test/documentation.spec.ts | 5 +++-- tools/docs/schema.ts | 8 +++++++- 6 files changed, 44 insertions(+), 5 deletions(-) diff --git a/lib/config/defaults.ts b/lib/config/defaults.ts index 1fd65e07b619c3..8aa2ffd77d883a 100644 --- a/lib/config/defaults.ts +++ b/lib/config/defaults.ts @@ -1,3 +1,4 @@ +import { dequal } from 'dequal'; import { getOptions } from './options'; import type { AllConfig, RenovateOptions } from './types'; @@ -23,7 +24,7 @@ export function getConfig(): AllConfig { const options = getOptions(); const config: AllConfig = {}; options.forEach((option) => { - if (!option.parents) { + if (!option.parents || dequal(option.parents, ['.'])) { config[option.name] = getDefault(option); } }); diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts index d2994100d0bf84..b225d9612432ea 100644 --- a/lib/config/options/index.ts +++ b/lib/config/options/index.ts @@ -173,6 +173,7 @@ const options: RenovateOptions[] = [ experimentalDescription: 'Config migration PRs are still being improved, in particular to reduce the amount of reordering and whitespace changes.', experimentalIssues: [16359], + parents: ['.'], }, { name: 'productLinks', @@ -232,6 +233,7 @@ const options: RenovateOptions[] = [ subType: 'string', allowString: true, cli: false, + parents: ['.'], }, { name: 'migratePresets', @@ -364,6 +366,7 @@ const options: RenovateOptions[] = [ 'If enabled, Renovate logs the fully resolved config for each repository, plus the fully resolved presets.', type: 'boolean', default: false, + parents: ['.'], }, { name: 'binarySource', @@ -434,6 +437,7 @@ const options: RenovateOptions[] = [ experimentalIssues: [23286], default: {}, mergeable: true, + parents: ['.'], }, { name: 'dockerChildPrefix', @@ -543,6 +547,7 @@ const options: RenovateOptions[] = [ type: 'string', allowedValues: ['auto', 'enabled', 'disabled'], default: 'auto', + parents: ['.'], }, { name: 'includeMirrors', @@ -649,6 +654,7 @@ const options: RenovateOptions[] = [ 'Whether to create a "Dependency Dashboard" issue in the repository.', type: 'boolean', default: false, + parents: ['.'], }, { name: 'dependencyDashboardApproval', @@ -663,12 +669,14 @@ const options: RenovateOptions[] = [ 'Set to `true` to let Renovate close the Dependency Dashboard issue if there are no more updates.', type: 'boolean', default: false, + parents: ['.'], }, { name: 'dependencyDashboardTitle', description: 'Title for the Dependency Dashboard issue.', type: 'string', default: `Dependency Dashboard`, + parents: ['.'], }, { name: 'dependencyDashboardHeader', @@ -677,12 +685,14 @@ const options: RenovateOptions[] = [ type: 'string', default: 'This issue lists Renovate updates and detected dependencies. Read the [Dependency Dashboard](https://docs.renovatebot.com/key-concepts/dashboard/) docs to learn more.', + parents: ['.'], }, { name: 'dependencyDashboardFooter', description: 'Any text added here will be placed last in the Dependency Dashboard issue body, with a divider separator before it.', type: 'string', + parents: ['.'], }, { name: 'dependencyDashboardLabels', @@ -691,6 +701,7 @@ const options: RenovateOptions[] = [ type: 'array', subType: 'string', default: null, + parents: ['.'], }, { name: 'dependencyDashboardOSVVulnerabilitySummary', @@ -700,6 +711,7 @@ const options: RenovateOptions[] = [ allowedValues: ['none', 'all', 'unresolved'], default: 'none', experimental: true, + parents: ['.'], }, { name: 'configWarningReuseIssue', @@ -707,6 +719,7 @@ const options: RenovateOptions[] = [ 'Set this to `false` to make Renovate create a new issue for each config warning, instead of reopening or reusing an existing issue.', type: 'boolean', default: true, + parents: ['.'], }, // encryption @@ -986,6 +999,7 @@ const options: RenovateOptions[] = [ subType: 'string', stage: 'package', cli: false, + parents: ['.'], }, { name: 'useBaseBranchConfig', @@ -994,6 +1008,7 @@ const options: RenovateOptions[] = [ type: 'string', allowedValues: ['merge', 'none'], default: 'none', + parents: ['.'], }, { name: 'gitAuthor', @@ -1033,6 +1048,7 @@ const options: RenovateOptions[] = [ subType: 'string', mergeable: false, stage: 'repository', + parents: ['.'], }, { name: 'includePaths', @@ -1932,6 +1948,7 @@ const options: RenovateOptions[] = [ 'Rate limit PRs to maximum x created per hour. 0 means no limit.', type: 'integer', default: 2, + parents: ['.'], }, { name: 'prConcurrentLimit', @@ -1939,6 +1956,7 @@ const options: RenovateOptions[] = [ 'Limit to a maximum of x concurrent branches/PRs. 0 means no limit.', type: 'integer', default: 10, + parents: ['.'], }, { name: 'branchConcurrentLimit', @@ -1946,6 +1964,7 @@ const options: RenovateOptions[] = [ 'Limit to a maximum of x concurrent branches. 0 means no limit, `null` (default) inherits value from `prConcurrentLimit`.', type: 'integer', default: null, // inherit prConcurrentLimit + parents: ['.'], }, { name: 'prPriority', @@ -2044,6 +2063,7 @@ const options: RenovateOptions[] = [ cli: false, env: false, supportedPlatforms: ['github'], + parents: ['.'], }, { name: 'osvVulnerabilityAlerts', @@ -2052,6 +2072,7 @@ const options: RenovateOptions[] = [ default: false, experimental: true, experimentalIssues: [20542], + parents: ['.'], }, { name: 'pruneBranchAfterAutomerge', @@ -2198,6 +2219,7 @@ const options: RenovateOptions[] = [ additionalProperties: { type: 'string', }, + parents: ['.'], }, { name: 'lockFileMaintenance', @@ -2427,6 +2449,7 @@ const options: RenovateOptions[] = [ stage: 'repository', cli: true, mergeable: true, + parents: ['.'], }, { name: 'hostType', @@ -2718,6 +2741,7 @@ const options: RenovateOptions[] = [ stage: 'package', cli: true, mergeable: true, + parents: ['.'], }, { name: 'customType', @@ -2936,6 +2960,7 @@ const options: RenovateOptions[] = [ experimental: true, globalOnly: true, default: [], + parents: ['.'], }, { name: 'maxRetryAfter', @@ -2956,6 +2981,7 @@ const options: RenovateOptions[] = [ stage: 'repository', cli: false, env: false, + parents: ['.'], }, { name: 'matchMessage', diff --git a/lib/config/types.ts b/lib/config/types.ts index b34d914addbdf2..d9c539093ffa41 100644 --- a/lib/config/types.ts +++ b/lib/config/types.ts @@ -387,6 +387,7 @@ export interface ValidationMessage { } export type AllowedParents = + | '.' // Root | 'customManagers' | 'customDatasources' | 'hostRules' diff --git a/lib/config/validation.ts b/lib/config/validation.ts index 7807c9efb49d65..825be8de1b922b 100644 --- a/lib/config/validation.ts +++ b/lib/config/validation.ts @@ -263,9 +263,13 @@ export async function validateConfig( !optionParents[key].includes(parentName as AllowedParents) ) { // TODO: types (#22198) - const message = `${key} should only be configured within one of "${optionParents[ + let message = `${key} should only be configured within one of "${optionParents[ key ]?.join(' or ')}" objects. Was found in ${parentName}`; + message = message.replace( + 'within one of "." objects', + 'at the top level', + ); warnings.push({ topic: `${parentPath ? `${parentPath}.` : ''}${key}`, message, diff --git a/test/documentation.spec.ts b/test/documentation.spec.ts index f0f66b1e94f86a..5105afcc061023 100644 --- a/test/documentation.spec.ts +++ b/test/documentation.spec.ts @@ -1,3 +1,4 @@ +import { dequal } from 'dequal'; import fs from 'fs-extra'; import { glob } from 'glob'; import { getOptions } from '../lib/config/options'; @@ -31,7 +32,7 @@ describe('documentation', () => { const expectedOptions = options .filter((option) => !option.globalOnly) - .filter((option) => !option.parents) + .filter((option) => !option.parents || dequal(option.parents, ['.'])) .filter((option) => !option.autogenerated) .map((option) => option.name) .sort(); @@ -51,7 +52,7 @@ describe('documentation', () => { const expectedSubOptions = options .filter((option) => option.stage !== 'global') .filter((option) => !option.globalOnly) - .filter((option) => option.parents) + .filter((option) => option.parents && !dequal(option.parents, ['.'])) .map((option) => option.name) .sort(); expectedSubOptions.sort(); diff --git a/tools/docs/schema.ts b/tools/docs/schema.ts index 293e713af91ba0..db1fc9b6fb8bbc 100644 --- a/tools/docs/schema.ts +++ b/tools/docs/schema.ts @@ -75,7 +75,7 @@ function createSingleConfig(option: RenovateOptions): Record { function createSchemaForParentConfigs(): void { for (const option of options) { - if (!option.parents) { + if (!option.parents || option.parents.includes('.')) { properties[option.name] = createSingleConfig(option); } } @@ -85,6 +85,9 @@ function addChildrenArrayInParents(): void { for (const option of options) { if (option.parents) { for (const parent of option.parents) { + if (parent === '.') { + continue; + } properties[parent].items = { allOf: [ { @@ -120,6 +123,9 @@ function createSchemaForChildConfigs(): void { for (const option of options) { if (option.parents) { for (const parent of option.parents) { + if (parent === '.') { + continue; + } properties[parent].items.allOf[0].properties[option.name] = createSingleConfig(option); } From 6b73d14cd25a2727a172f3307633d5380dfd0266 Mon Sep 17 00:00:00 2001 From: Rhys Arkins Date: Sat, 13 Apr 2024 07:40:03 +0200 Subject: [PATCH 2/3] DRY --- lib/config/defaults.ts | 4 ++-- lib/config/options/index.ts | 4 ++++ test/documentation.spec.ts | 6 +++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/config/defaults.ts b/lib/config/defaults.ts index 8aa2ffd77d883a..64b0d8306101a6 100644 --- a/lib/config/defaults.ts +++ b/lib/config/defaults.ts @@ -1,5 +1,5 @@ import { dequal } from 'dequal'; -import { getOptions } from './options'; +import { getOptions, isTopLevelOnlyOption } from './options'; import type { AllConfig, RenovateOptions } from './types'; // Use functions instead of direct values to avoid introducing global references. @@ -24,7 +24,7 @@ export function getConfig(): AllConfig { const options = getOptions(); const config: AllConfig = {}; options.forEach((option) => { - if (!option.parents || dequal(option.parents, ['.'])) { + if (!option.parents || isTopLevelOnlyOption(option)) { config[option.name] = getDefault(option); } }); diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts index b225d9612432ea..54b3da0071eeff 100644 --- a/lib/config/options/index.ts +++ b/lib/config/options/index.ts @@ -3013,6 +3013,10 @@ export function getOptions(): RenovateOptions[] { return options; } +export function isTopLevelOnlyOption(option: RenovateOptions): boolean { + return option.parents?.length === 1 && option.parents[0] === '.'; +} + function loadManagerOptions(): void { const allManagers = new Map([...getManagers(), ...getCustomManagers()]); for (const [name, config] of allManagers.entries()) { diff --git a/test/documentation.spec.ts b/test/documentation.spec.ts index 5105afcc061023..6d0f556463be2d 100644 --- a/test/documentation.spec.ts +++ b/test/documentation.spec.ts @@ -1,7 +1,7 @@ import { dequal } from 'dequal'; import fs from 'fs-extra'; import { glob } from 'glob'; -import { getOptions } from '../lib/config/options'; +import { getOptions, isTopLevelOnlyOption } from '../lib/config/options'; import { regEx } from '../lib/util/regex'; const options = getOptions(); @@ -32,7 +32,7 @@ describe('documentation', () => { const expectedOptions = options .filter((option) => !option.globalOnly) - .filter((option) => !option.parents || dequal(option.parents, ['.'])) + .filter((option) => !option.parents || isTopLevelOnlyOption(option)) .filter((option) => !option.autogenerated) .map((option) => option.name) .sort(); @@ -52,7 +52,7 @@ describe('documentation', () => { const expectedSubOptions = options .filter((option) => option.stage !== 'global') .filter((option) => !option.globalOnly) - .filter((option) => option.parents && !dequal(option.parents, ['.'])) + .filter((option) => option.parents && !isTopLevelOnlyOption(option)) .map((option) => option.name) .sort(); expectedSubOptions.sort(); From f151414bbe14560965623284323aacc15d198028 Mon Sep 17 00:00:00 2001 From: Rhys Arkins Date: Sat, 13 Apr 2024 07:40:42 +0200 Subject: [PATCH 3/3] drop dequal --- lib/config/defaults.ts | 1 - test/documentation.spec.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/lib/config/defaults.ts b/lib/config/defaults.ts index 64b0d8306101a6..620d94c6d79efc 100644 --- a/lib/config/defaults.ts +++ b/lib/config/defaults.ts @@ -1,4 +1,3 @@ -import { dequal } from 'dequal'; import { getOptions, isTopLevelOnlyOption } from './options'; import type { AllConfig, RenovateOptions } from './types'; diff --git a/test/documentation.spec.ts b/test/documentation.spec.ts index 6d0f556463be2d..4f69e72e879f31 100644 --- a/test/documentation.spec.ts +++ b/test/documentation.spec.ts @@ -1,4 +1,3 @@ -import { dequal } from 'dequal'; import fs from 'fs-extra'; import { glob } from 'glob'; import { getOptions, isTopLevelOnlyOption } from '../lib/config/options';