Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(config/inherit): resolve presets #31642

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/usage/config-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
rarkins marked this conversation as resolved.
Show resolved Hide resolved

RahulGautamSingh marked this conversation as resolved.
Show resolved Hide resolved
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.
Expand Down
136 changes: 135 additions & 1 deletion lib/workers/repository/init/inherited.spec.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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;

Expand Down Expand Up @@ -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.',
RahulGautamSingh marked this conversation as resolved.
Show resolved Hide resolved
);
});

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.',
);
});
});
40 changes: 39 additions & 1 deletion lib/workers/repository/init/inherited.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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.',
);
}
RahulGautamSingh marked this conversation as resolved.
Show resolved Hide resolved

// 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);
}