diff --git a/docs/usage/self-hosted-configuration.md b/docs/usage/self-hosted-configuration.md index 2e19b6b341a0dddb2f65de8dd834c6598b091b5c..66b65a93204db02e8ec31c41a6edd017d713a648 100644 --- a/docs/usage/self-hosted-configuration.md +++ b/docs/usage/self-hosted-configuration.md @@ -353,6 +353,13 @@ Possible values: - `ssh`: use SSH URLs provided by the platform for Git - `endpoint`: ignore URLs provided by the platform and use the configured endpoint directly +## globalExtends + +Unlike the `extends` field, which is passed through unresolved to be part of repository config, any presets in `globalExtends` are resolved immediately as part of global config. +Therefore you need to use this field if your preset contains any global-only configuration options, such as the list of repositories to run against. + +Use the `extends` field instead of this if, for example, you need the ability for a repository config (e.g. `renovate.json`) to be able to use `ignorePresets` for any preset defined in global config. + ## logContext `logContext` is included with each log entry only if `logFormat="json"` - it is not included in the pretty log output. diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts index 1816a3e154255efe0fca0ee2291655fe31892a53..438453110a0792e56d5772285f141c7ab64f2e0e 100644 --- a/lib/config/options/index.ts +++ b/lib/config/options/index.ts @@ -181,6 +181,14 @@ const options: RenovateOptions[] = [ type: 'string', }, }, + { + name: 'globalExtends', + description: + 'Configuration presets to use/extend for a self-hosted config.', + type: 'array', + subType: 'string', + globalOnly: true, + }, { name: 'description', description: 'Plain text description for a config or preset.', diff --git a/lib/config/types.ts b/lib/config/types.ts index e792be3aa0939d45798c59ef448ed5ee3db11fc9..6fe7a9452a69b7233b5f8dfa15c6f3f8afd146cf 100644 --- a/lib/config/types.ts +++ b/lib/config/types.ts @@ -81,6 +81,7 @@ export interface GlobalOnlyConfig { forceCli?: boolean; gitNoVerify?: GitNoVerifyOption[]; gitPrivateKey?: string; + globalExtends?: string[]; logFile?: string; logFileLevel?: LogLevel; prCommitsPerRunLimit?: number; diff --git a/lib/workers/global/index.spec.ts b/lib/workers/global/index.spec.ts index 237bcd76690309d258e4f0002368dc046a0a23c4..b1d7df99b4bd5a2a26c9d659ed05b99cad249271 100644 --- a/lib/workers/global/index.spec.ts +++ b/lib/workers/global/index.spec.ts @@ -1,7 +1,9 @@ import { expect } from '@jest/globals'; import { ERROR, WARN } from 'bunyan'; -import { fs, logger } from '../../../test/util'; +import { fs, logger, mocked } from '../../../test/util'; +import * as _presets from '../../config/presets'; import { PlatformId } from '../../constants'; +import { CONFIG_PRESETS_INVALID } from '../../constants/error-messages'; import * as datasourceDocker from '../../datasource/docker'; import * as _platform from '../../platform'; import * as _repositoryWorker from '../repository'; @@ -11,11 +13,13 @@ import * as globalWorker from '.'; jest.mock('../repository'); jest.mock('../../util/fs'); +jest.mock('../../config/presets'); // imports are readonly const repositoryWorker = _repositoryWorker; const configParser: jest.Mocked<typeof _configParser> = _configParser as never; const platform: jest.Mocked<typeof _platform> = _platform as never; +const presets = mocked(_presets); const limits = _limits; describe('workers/global/index', () => { @@ -25,6 +29,7 @@ describe('workers/global/index', () => { configParser.parseConfigs = jest.fn(); platform.initPlatform.mockImplementation((input) => Promise.resolve(input)); }); + it('handles config warnings and errors', async () => { configParser.parseConfigs.mockResolvedValueOnce({ repositories: [], @@ -33,6 +38,33 @@ describe('workers/global/index', () => { }); await expect(globalWorker.start()).resolves.toBe(0); }); + + it('resolves global presets immediately', async () => { + configParser.parseConfigs.mockResolvedValueOnce({ + repositories: [], + globalExtends: [':pinVersions'], + }); + presets.resolveConfigPresets.mockResolvedValueOnce({}); + await expect(globalWorker.start()).resolves.toBe(0); + expect(presets.resolveConfigPresets).toHaveBeenCalledWith({ + extends: [':pinVersions'], + }); + }); + + it('throws if global presets could not be resolved', async () => { + configParser.parseConfigs.mockResolvedValueOnce({ + repositories: [], + globalExtends: [':pinVersions'], + }); + presets.resolveConfigPresets.mockImplementation(() => { + throw new Error('some-error'); + }); + await expect( + globalWorker.resolveGlobalExtends(['some-preset']) + ).rejects.toThrow(CONFIG_PRESETS_INVALID); + expect(presets.resolveConfigPresets).toHaveBeenCalled(); + }); + it('handles zero repos', async () => { configParser.parseConfigs.mockResolvedValueOnce({ baseDir: '/tmp/base', @@ -41,6 +73,7 @@ describe('workers/global/index', () => { }); await expect(globalWorker.start()).resolves.toBe(0); }); + it('processes repositories', async () => { configParser.parseConfigs.mockResolvedValueOnce({ gitAuthor: 'a@b.com', diff --git a/lib/workers/global/index.ts b/lib/workers/global/index.ts index 36222ffd01a10cde50823c2aef4827c4231fbafd..1470d4efe49d6f1d9f9828c69f07d248a9aa5162 100644 --- a/lib/workers/global/index.ts +++ b/lib/workers/global/index.ts @@ -4,6 +4,7 @@ import fs from 'fs-extra'; import semver from 'semver'; import upath from 'upath'; import * as configParser from '../../config'; +import { mergeChildConfig } from '../../config'; import { resolveConfigPresets } from '../../config/presets'; import { validateConfigSecrets } from '../../config/secrets'; import type { @@ -85,11 +86,32 @@ export async function validatePresets(config: AllConfig): Promise<void> { } } +export async function resolveGlobalExtends( + globalExtends: string[] +): Promise<AllConfig> { + try { + // Make a "fake" config to pass to resolveConfigPresets and resolve globalPresets + const config = { extends: globalExtends }; + const resolvedConfig = await resolveConfigPresets(config); + return resolvedConfig; + } catch (err) { + logger.error({ err }, 'Error resolving config preset'); + throw new Error(CONFIG_PRESETS_INVALID); + } +} + export async function start(): Promise<number> { let config: AllConfig; try { // read global config from file, env and cli args config = await getGlobalConfig(); + if (config?.globalExtends) { + // resolve global presets immediately + config = mergeChildConfig( + config, + await resolveGlobalExtends(config.globalExtends) + ); + } // initialize all submodules config = await globalInitialize(config);