Skip to content

Commit

Permalink
feat(config): inheritConfig (#27864)
Browse files Browse the repository at this point in the history
Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com>
Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
Co-authored-by: Sebastian Poxhofer <secustor@users.noreply.github.com>
  • Loading branch information
4 people committed Mar 23, 2024
1 parent d953196 commit e4ab753
Show file tree
Hide file tree
Showing 11 changed files with 400 additions and 20 deletions.
52 changes: 52 additions & 0 deletions docs/usage/self-hosted-configuration.md
Expand Up @@ -672,6 +672,58 @@ By default, Renovate does not autodiscover repositories that are mirrors.

Change this setting to `true` to include repositories that are mirrors as Renovate targets.

## inheritConfig

When you enable this option, Renovate will look for the `inheritConfigFileName` file in the `inheritConfigRepoName` repository before processing a repository, and read this in as config.

If the repository is in a nested organization or group on a supported platform such as GitLab, such as `topGroup/nestedGroup/projectName` then Renovate will look in `topGroup/nestedGroup/renovate-config`.

If `inheritConfig` is `true` but the inherited config file does _not_ exist then Renovate will proceed without warning.
If the file exists but cannot be parsed, then Renovate will raise a config warning issue and abort the job.

The inherited config may include all valid repository config and these config options:

- `bbUseDevelopmentBranch`
- `onboarding`
- `onboardingBranch`
- `onboardingCommitMessage`
- `onboardingConfig`
- `onboardingConfigFileName`
- `onboardingNoDeps`
- `onboardingPrTitle`
- `onboardingRebaseCheckbox`
- `requireConfig`

<!-- prettier-ignore -->
!!! note
The above list is prepared manually and may become out of date.
Consult the self-hosted configuration docs and look for `inheritConfigSupport` values there for the definitive list.

This way organizations can change/control the default behavior, like whether configs are required and how repositories are onboarded.

We disabled `inheritConfig` in the Mend Renovate App to avoid wasting millions of API calls per week.
This is because each `404` response from the GitHub API due to a missing org inherited config counts as a used API call.
We will add a smart/dynamic approach in future, so that we can selectively enable `inheritConfig` per organization.

## inheritConfigFileName

Change this setting if you want Renovate to look for a different file name within the `inheritConfigRepoName` repository.
You may use nested files, for example: `"some-dir/config.json"`.

## inheritConfigRepoName

Change this setting if you want Renovate to look in an alternative repository for the inherited config.
The repository must be on the same platform and endpoint, and Renovate's token must have `read` permissions to the repository.

## inheritConfigStrict

By default Renovate will silently (debug log message only) ignore cases where `inheritConfig=true` but no inherited config is found.
When you set `inheritConfigStrict=true` then Renovate will abort the run and raise a config error if Renovate can't find the inherited config.

<!-- prettier-ignore -->
!!! warning
Only set this config option to `true` if _every_ organization has an inherited config file _and_ you want to make sure Renovate _always_ uses that inherited config.

## logContext

`logContext` is included with each log entry only if `logFormat="json"` - it is not included in the pretty log output.
Expand Down
23 changes: 22 additions & 1 deletion lib/config/index.spec.ts
@@ -1,5 +1,10 @@
import { getConfig } from './defaults';
import { filterConfig, getManagerConfig, mergeChildConfig } from './index';
import {
filterConfig,
getManagerConfig,
mergeChildConfig,
removeGlobalConfig,
} from './index';

jest.mock('../modules/datasource/npm');
jest.mock('../../config.js', () => ({}), { virtual: true });
Expand Down Expand Up @@ -131,4 +136,20 @@ describe('config/index', () => {
expect(config.vulnerabilitySeverity).toBe('CRITICAL');
});
});

describe('removeGlobalConfig()', () => {
it('removes all global config', () => {
const filteredConfig = removeGlobalConfig(defaultConfig, false);
expect(filteredConfig).not.toHaveProperty('onboarding');
expect(filteredConfig).not.toHaveProperty('binarySource');
expect(filteredConfig.prHourlyLimit).toBe(2);
});

it('retains inherited config', () => {
const filteredConfig = removeGlobalConfig(defaultConfig, true);
expect(filteredConfig).toHaveProperty('onboarding');
expect(filteredConfig).not.toHaveProperty('binarySource');
expect(filteredConfig.prHourlyLimit).toBe(2);
});
});
});
17 changes: 17 additions & 0 deletions lib/config/index.ts
Expand Up @@ -31,6 +31,22 @@ export function getManagerConfig(
return managerConfig;
}

export function removeGlobalConfig(
config: RenovateConfig,
keepInherited: boolean,
): RenovateConfig {
const outputConfig: RenovateConfig = { ...config };
for (const option of options.getOptions()) {
if (keepInherited && option.inheritConfigSupport) {
continue;
}
if (option.globalOnly) {
delete outputConfig[option.name];
}
}
return outputConfig;
}

export function filterConfig(
inputConfig: AllConfig,
targetStage: RenovateConfigStage,
Expand All @@ -39,6 +55,7 @@ export function filterConfig(
const outputConfig: RenovateConfig = { ...inputConfig };
const stages: (string | undefined)[] = [
'global',
'inherit',
'repository',
'package',
'branch',
Expand Down
41 changes: 41 additions & 0 deletions lib/config/options/index.ts
Expand Up @@ -122,6 +122,7 @@ const options: RenovateOptions[] = [
type: 'string',
default: 'renovate/configure',
globalOnly: true,
inheritConfigSupport: true,
cli: false,
},
{
Expand All @@ -131,6 +132,7 @@ const options: RenovateOptions[] = [
type: 'string',
default: null,
globalOnly: true,
inheritConfigSupport: true,
cli: false,
},
{
Expand All @@ -140,6 +142,7 @@ const options: RenovateOptions[] = [
type: 'string',
default: 'renovate.json',
globalOnly: true,
inheritConfigSupport: true,
cli: false,
},
{
Expand All @@ -148,6 +151,7 @@ const options: RenovateOptions[] = [
type: 'boolean',
default: false,
globalOnly: true,
inheritConfigSupport: true,
},
{
name: 'onboardingPrTitle',
Expand All @@ -156,6 +160,7 @@ const options: RenovateOptions[] = [
type: 'string',
default: 'Configure Renovate',
globalOnly: true,
inheritConfigSupport: true,
cli: false,
},
{
Expand Down Expand Up @@ -507,6 +512,7 @@ const options: RenovateOptions[] = [
stage: 'repository',
type: 'boolean',
globalOnly: true,
inheritConfigSupport: true,
},
{
name: 'onboardingConfig',
Expand All @@ -515,6 +521,7 @@ const options: RenovateOptions[] = [
type: 'object',
default: { $schema: 'https://docs.renovatebot.com/renovate-schema.json' },
globalOnly: true,
inheritConfigSupport: true,
mergeable: true,
},
{
Expand Down Expand Up @@ -583,6 +590,38 @@ const options: RenovateOptions[] = [
default: true,
globalOnly: true,
},
{
name: 'inheritConfig',
description:
'If `true`, Renovate will inherit configuration from the `inheritConfigFileName` file in `inheritConfigRepoName',
type: 'boolean',
default: false,
globalOnly: true,
},
{
name: 'inheritConfigRepoName',
description:
'Renovate will look in this repo for the `inheritConfigFileName`.',
type: 'string',
default: '{{parentOrg}}/renovate-config',
globalOnly: true,
},
{
name: 'inheritConfigFileName',
description:
'Renovate will look for this config file name in the `inheritConfigRepoName`.',
type: 'string',
default: 'org-inherited-config.json',
globalOnly: true,
},
{
name: 'inheritConfigStrict',
description:
'If `true`, any `inheritedConfig` fetch errror will result in an aborted run.',
type: 'boolean',
default: false,
globalOnly: true,
},
{
name: 'requireConfig',
description:
Expand All @@ -592,6 +631,7 @@ const options: RenovateOptions[] = [
default: 'required',
allowedValues: ['required', 'optional', 'ignored'],
globalOnly: true,
inheritConfigSupport: true,
},
{
name: 'optimizeForDisabled',
Expand Down Expand Up @@ -1921,6 +1961,7 @@ const options: RenovateOptions[] = [
default: false,
supportedPlatforms: ['bitbucket'],
globalOnly: true,
inheritConfigSupport: true,
},
// Automatic merging
{
Expand Down
8 changes: 8 additions & 0 deletions lib/config/types.ts
Expand Up @@ -8,6 +8,7 @@ import type { MergeConfidence } from '../util/merge-confidence/types';

export type RenovateConfigStage =
| 'global'
| 'inherit'
| 'repository'
| 'package'
| 'branch'
Expand Down Expand Up @@ -232,6 +233,11 @@ export interface RenovateConfig

hostRules?: HostRule[];

inheritConfig?: boolean;
inheritConfigFileName?: string;
inheritConfigRepoName?: string;
inheritConfigStrict?: boolean;

ignorePresets?: string[];
forkProcessing?: 'auto' | 'enabled' | 'disabled';
isFork?: boolean;
Expand Down Expand Up @@ -394,6 +400,8 @@ export interface RenovateOptionBase {
*/
globalOnly?: boolean;

inheritConfigSupport?: boolean;

allowedValues?: string[];

allowString?: boolean;
Expand Down
39 changes: 35 additions & 4 deletions lib/config/validation.spec.ts
Expand Up @@ -45,10 +45,30 @@ describe('config/validation', () => {
expect(warnings).toHaveLength(2);
expect(warnings).toMatchObject([
{
message: `The "binarySource" option is a global option reserved only for Renovate's global configuration and cannot be configured within repository config file.`,
message: `The "binarySource" option is a global option reserved only for Renovate's global configuration and cannot be configured within a repository's config file.`,
},
{
message: `The "username" option is a global option reserved only for Renovate's global configuration and cannot be configured within repository config file.`,
message: `The "username" option is a global option reserved only for Renovate's global configuration and cannot be configured within a repository's config file.`,
},
]);
});

it('catches global options in inherit config', async () => {
const config = {
binarySource: 'something',
username: 'user',
};
const { warnings } = await configValidation.validateConfig(
'inherit',
config,
);
expect(warnings).toHaveLength(2);
expect(warnings).toMatchObject([
{
message: `The "binarySource" option is a global option reserved only for Renovate's global configuration and cannot be configured within a repository's config file.`,
},
{
message: `The "username" option is a global option reserved only for Renovate's global configuration and cannot be configured within a repository's config file.`,
},
]);
});
Expand All @@ -70,6 +90,17 @@ describe('config/validation', () => {
expect(warnings).toHaveLength(0);
});

it('does not warn for valid inheritConfig', async () => {
const config = {
onboarding: false,
};
const { warnings } = await configValidation.validateConfig(
'inherit',
config,
);
expect(warnings).toHaveLength(0);
});

it('catches invalid templates', async () => {
const config = {
commitMessage: '{{{something}}',
Expand Down Expand Up @@ -1020,7 +1051,7 @@ describe('config/validation', () => {
expect(warnings).toMatchObject([
{
topic: 'Configuration Error',
message: `The "customEnvVariables" option is a global option reserved only for Renovate's global configuration and cannot be configured within repository config file.`,
message: `The "customEnvVariables" option is a global option reserved only for Renovate's global configuration and cannot be configured within a repository's config file.`,
},
]);
});
Expand Down Expand Up @@ -1426,7 +1457,7 @@ describe('config/validation', () => {
},
{
topic: 'Configuration Error',
message: `The "binarySource" option is a global option reserved only for Renovate's global configuration and cannot be configured within repository config file.`,
message: `The "binarySource" option is a global option reserved only for Renovate's global configuration and cannot be configured within a repository's config file.`,
},
]);
});
Expand Down

0 comments on commit e4ab753

Please sign in to comment.