From eb074924655488bbd62dba7f55e75bfb925e0f94 Mon Sep 17 00:00:00 2001 From: RahulGautamSingh Date: Fri, 13 Dec 2024 01:53:03 +0530 Subject: [PATCH] fix(config/inherit): resolve presets (#31642) Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com> Co-authored-by: Rhys Arkins --- docs/usage/config-overview.md | 13 ++ lib/workers/repository/init/inherited.spec.ts | 136 +++++++++++++++++- lib/workers/repository/init/inherited.ts | 40 +++++- 3 files changed, 187 insertions(+), 2 deletions(-) diff --git a/docs/usage/config-overview.md b/docs/usage/config-overview.md index 99d6b40c96a26f..669e15ea87607b 100644 --- a/docs/usage/config-overview.md +++ b/docs/usage/config-overview.md @@ -159,6 +159,19 @@ Inherited config may use all Repository config settings, and any Global config o For information on how the Mend Renovate App supports Inherited config, see the dedicated "Mend Renovate App Config" section toward the end of this page. +#### Presets handling + +If the inherited config contains `extends` presets, then Renovate will: + +1. Resolve the presets +1. Add the resolved preset config to the beginning of the inherited config +1. Merge the presets on top of the global config + +##### You can not ignore presets from inherited config + +You can _not_ use `ignorePresets` in your repository config to ignore presets _within_ inherited config. +This is because inherited config is resolved _before_ the repository config. + ### Repository config Repository config is the config loaded from a config file in the repository. diff --git a/lib/workers/repository/init/inherited.spec.ts b/lib/workers/repository/init/inherited.spec.ts index f89c18201649c8..e31f5ba2fc32bf 100644 --- a/lib/workers/repository/init/inherited.spec.ts +++ b/lib/workers/repository/init/inherited.spec.ts @@ -1,5 +1,7 @@ -import { platform } from '../../../../test/util'; +import { mocked, platform } from '../../../../test/util'; +import * as presets_ from '../../../config/presets'; import type { RenovateConfig } from '../../../config/types'; +import * as validation from '../../../config/validation'; import { CONFIG_INHERIT_NOT_FOUND, CONFIG_INHERIT_PARSE_ERROR, @@ -8,6 +10,10 @@ import { import { logger } from '../../../logger'; import { mergeInheritedConfig } from './inherited'; +jest.mock('../../../config/presets'); + +const presets = mocked(presets_); + describe('workers/repository/init/inherited', () => { let config: RenovateConfig; @@ -84,4 +90,132 @@ describe('workers/repository/init/inherited', () => { expect(res.onboarding).toBeFalse(); expect(logger.warn).not.toHaveBeenCalled(); }); + + it('should resolve presets found in inherited config', async () => { + platform.getRawFile.mockResolvedValue( + '{"onboarding":false,"labels":["test"],"extends":[":automergeAll"]}', + ); + presets.resolveConfigPresets.mockResolvedValue({ + onboarding: false, + labels: ['test'], + automerge: true, + }); + const res = await mergeInheritedConfig(config); + expect(res.labels).toEqual(['test']); + expect(res.onboarding).toBeFalse(); + expect(logger.warn).not.toHaveBeenCalled(); + expect(logger.debug).toHaveBeenCalledWith( + 'Resolving presets found in inherited config', + ); + }); + + it('should warn if presets fails validation with warnings', async () => { + platform.getRawFile.mockResolvedValue( + '{"onboarding":false,"labels":["test"],"extends":[":automergeAll"]}', + ); + jest + .spyOn(validation, 'validateConfig') + .mockResolvedValueOnce({ + warnings: [], + errors: [], + }) + .mockResolvedValueOnce({ + warnings: [ + { + message: 'some warning', + topic: 'Configuration Error', + }, + ], + errors: [], + }); + presets.resolveConfigPresets.mockResolvedValue({ + onboarding: false, + labels: ['test'], + automerge: true, + }); + const res = await mergeInheritedConfig(config); + expect(res.binarySource).toBeUndefined(); + expect(logger.warn).toHaveBeenCalledWith( + { + warnings: [ + { + message: 'some warning', + topic: 'Configuration Error', + }, + ], + }, + 'Found warnings in presets inside the inherited configuration.', + ); + }); + + it('should throw error if presets fails validation with errors', async () => { + platform.getRawFile.mockResolvedValue( + '{"labels":["test"],"extends":[":automergeAll"]}', + ); + jest + .spyOn(validation, 'validateConfig') + .mockResolvedValueOnce({ + warnings: [], + errors: [], + }) + .mockResolvedValueOnce({ + warnings: [], + errors: [ + { + message: 'some error', + topic: 'Configuration Error', + }, + ], + }); + presets.resolveConfigPresets.mockResolvedValue({ + labels: ['test'], + automerge: true, + }); + await expect(mergeInheritedConfig(config)).rejects.toThrow( + CONFIG_VALIDATION, + ); + expect(logger.warn).toHaveBeenCalledWith( + { + errors: [ + { + message: 'some error', + topic: 'Configuration Error', + }, + ], + }, + 'Found errors in presets inside the inherited configuration.', + ); + }); + + it('should remove global config from presets found in inherited config', async () => { + platform.getRawFile.mockResolvedValue( + '{"labels":["test"],"extends":[":automergeAll"]}', + ); + jest.spyOn(validation, 'validateConfig').mockResolvedValue({ + warnings: [], + errors: [], + }); + presets.resolveConfigPresets.mockResolvedValue({ + labels: ['test'], + automerge: true, + binarySource: 'docker', // global config option: should not be here + }); + const res = await mergeInheritedConfig(config); + expect(res.labels).toEqual(['test']); + expect(logger.warn).not.toHaveBeenCalled(); + expect(logger.debug).toHaveBeenCalledWith( + { + inheritedConfig: { + labels: ['test'], + automerge: true, + binarySource: 'docker', + }, + filteredConfig: { + labels: ['test'], + automerge: true, + }, + }, + 'Removed global config from inherited config presets.', + ); + }); }); diff --git a/lib/workers/repository/init/inherited.ts b/lib/workers/repository/init/inherited.ts index f25be4f3f3169e..d924ff825bd37d 100644 --- a/lib/workers/repository/init/inherited.ts +++ b/lib/workers/repository/init/inherited.ts @@ -2,6 +2,7 @@ import is from '@sindresorhus/is'; import { dequal } from 'dequal'; import { mergeChildConfig, removeGlobalConfig } from '../../../config'; import { parseFileConfig } from '../../../config/parse'; +import { resolveConfigPresets } from '../../../config/presets'; import type { RenovateConfig } from '../../../config/types'; import { validateConfig } from '../../../config/validation'; import { @@ -92,12 +93,49 @@ export async function mergeInheritedConfig( 'Found warnings in inherited configuration.', ); } - const filteredConfig = removeGlobalConfig(inheritedConfig, true); + let filteredConfig = removeGlobalConfig(inheritedConfig, true); if (!dequal(inheritedConfig, filteredConfig)) { logger.debug( { inheritedConfig, filteredConfig }, 'Removed global config from inherited config.', ); } + + if (is.nullOrUndefined(filteredConfig.extends)) { + return mergeChildConfig(config, filteredConfig); + } + + logger.debug('Resolving presets found in inherited config'); + const resolvedConfig = await resolveConfigPresets( + filteredConfig, + config, + config.ignorePresets, + ); + logger.trace({ config: resolvedConfig }, 'Resolved inherited config'); + + const validationRes = await validateConfig('inherit', resolvedConfig); + if (validationRes.errors.length) { + logger.warn( + { errors: validationRes.errors }, + 'Found errors in presets inside the inherited configuration.', + ); + throw new Error(CONFIG_VALIDATION); + } + if (validationRes.warnings.length) { + logger.warn( + { warnings: validationRes.warnings }, + 'Found warnings in presets inside the inherited configuration.', + ); + } + + // remove global config options once again, as resolved presets could have added some + filteredConfig = removeGlobalConfig(resolvedConfig, true); + if (!dequal(resolvedConfig, filteredConfig)) { + logger.debug( + { inheritedConfig: resolvedConfig, filteredConfig }, + 'Removed global config from inherited config presets.', + ); + } + return mergeChildConfig(config, filteredConfig); }