diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md index 093658d1432bf39d6d54afa9523da124e24301a8..a0b519f6113340e31a547df89e1cf4f490f3fdd6 100644 --- a/docs/usage/configuration-options.md +++ b/docs/usage/configuration-options.md @@ -244,10 +244,29 @@ Renovate also allows users to explicitly configure `baseBranches`, e.g. for use - You wish Renovate to process only a non-default branch, e.g. `dev`: `"baseBranches": ["dev"]` - You have multiple release streams you need Renovate to keep up to date, e.g. in branches `main` and `next`: `"baseBranches": ["main", "next"]` +- You want to update your main branch and consistently named release branches, e.g. `main` and `release/<version>`: `"baseBranches": ["main", "/^release\\/.*/"]` It's possible to add this setting into the `renovate.json` file as part of the "Configure Renovate" onboarding PR. If so then Renovate will reflect this setting in its description and use package file contents from the custom base branch(es) instead of default. +`baseBranches` supports Regular Expressions that must begin and end with `/`, e.g.: + +```json +{ + "baseBranches": ["main", "/^release\\/.*/"] +} +``` + +You can negate the regex by prefixing it with `!`. +Only use a single negation and do not mix it with other branch names, since all branches are combined with `or`. +With a negation, all branches except those matching the regex will be added to the result: + +```json +{ + "baseBranches": ["!/^pre-release\\/.*/"] +} +``` + <!-- prettier-ignore --> !!! note Do _not_ use the `baseBranches` config option when you've set a `forkToken`. diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts index 7acc420308e7e95d9edcb954340b0ca3580d6309..72a19f22ba1163024abc3f6e682cc27e94a4492b 100644 --- a/lib/config/options/index.ts +++ b/lib/config/options/index.ts @@ -749,8 +749,9 @@ const options: RenovateOptions[] = [ { name: 'baseBranches', description: - 'An array of one or more custom base branches to be processed. If left empty, the default branch will be chosen.', + 'List of one or more custom base branches defined as exact strings and/or via regex expressions.', type: 'array', + subType: 'string', stage: 'package', cli: false, }, diff --git a/lib/config/validation.spec.ts b/lib/config/validation.spec.ts index 3574e7619ce10771c2eedef0363e0db62266707d..fa2d0c2a6041c7b3c1e8154eae91ca0c6612db07 100644 --- a/lib/config/validation.spec.ts +++ b/lib/config/validation.spec.ts @@ -117,6 +117,19 @@ describe('config/validation', () => { expect(errors).toMatchSnapshot(); }); + it('catches invalid baseBranches regex', async () => { + const config = { + baseBranches: ['/***$}{]][/'], + }; + const { errors } = await configValidation.validateConfig(config); + expect(errors).toEqual([ + { + topic: 'Configuration Error', + message: 'Invalid regExp for baseBranches: `/***$}{]][/`', + }, + ]); + }); + it('returns nested errors', async () => { const config: RenovateConfig = { foo: 1, diff --git a/lib/config/validation.ts b/lib/config/validation.ts index b98fae84d9f67926dd518f08ac757b039743d773..52d7e9a1deda0a42085b100950b0cb17821a2828 100644 --- a/lib/config/validation.ts +++ b/lib/config/validation.ts @@ -522,6 +522,19 @@ export async function validateConfig( } } } + if (key === 'baseBranches') { + for (const baseBranch of val as string[]) { + if ( + isConfigRegex(baseBranch) && + !configRegexPredicate(baseBranch) + ) { + errors.push({ + topic: 'Configuration Error', + message: `Invalid regExp for ${currentPath}: \`${baseBranch}\``, + }); + } + } + } if ( (selectors.includes(key) || key === 'matchCurrentVersion' || diff --git a/lib/workers/repository/process/index.spec.ts b/lib/workers/repository/process/index.spec.ts index 0e45a6058e78b6722e07f84025f06aa06b53da96..8573c31713e8a4c3408311fd263246579fa28463 100644 --- a/lib/workers/repository/process/index.spec.ts +++ b/lib/workers/repository/process/index.spec.ts @@ -2,6 +2,7 @@ import { RenovateConfig, getConfig, git, + logger, mocked, platform, } from '../../../../test/util'; @@ -120,5 +121,33 @@ describe('workers/repository/process/index', () => { }); expect(lookup).toHaveBeenCalledTimes(0); }); + + it('finds baseBranches via regular expressions', async () => { + extract.mockResolvedValue({} as never); + config.baseBranches = ['/^release\\/.*/', 'dev', '!/^pre-release\\/.*/']; + git.getBranchList.mockReturnValue([ + 'dev', + 'pre-release/v0', + 'release/v1', + 'release/v2', + 'some-other', + ]); + git.branchExists.mockReturnValue(true); + const res = await extractDependencies(config); + expect(res).toStrictEqual({ + branchList: [undefined, undefined, undefined, undefined], + branches: [undefined, undefined, undefined, undefined], + packageFiles: undefined, + }); + + expect(logger.logger.debug).toHaveBeenCalledWith( + { baseBranches: ['release/v1', 'release/v2', 'dev', 'some-other'] }, + 'baseBranches' + ); + expect(addMeta).toHaveBeenCalledWith({ baseBranch: 'release/v1' }); + expect(addMeta).toHaveBeenCalledWith({ baseBranch: 'release/v2' }); + expect(addMeta).toHaveBeenCalledWith({ baseBranch: 'dev' }); + expect(addMeta).toHaveBeenCalledWith({ baseBranch: 'some-other' }); + }); }); }); diff --git a/lib/workers/repository/process/index.ts b/lib/workers/repository/process/index.ts index b1cd6d8ae4930491ddf2dd5296f8826c0e3e7044..c4c5357895bc1a5071358baa744d0e60001ba466 100644 --- a/lib/workers/repository/process/index.ts +++ b/lib/workers/repository/process/index.ts @@ -8,7 +8,8 @@ import type { PackageFile } from '../../../modules/manager/types'; import { platform } from '../../../modules/platform'; import { getCache } from '../../../util/cache/repository'; import { clone } from '../../../util/clone'; -import { branchExists } from '../../../util/git'; +import { branchExists, getBranchList } from '../../../util/git'; +import { configRegexPredicate } from '../../../util/regex'; import { addSplit } from '../../../util/split'; import type { BranchConfig } from '../../types'; import { readDashboardBody } from '../dependency-dashboard'; @@ -81,6 +82,23 @@ async function getBaseBranchConfig( return baseBranchConfig; } +function unfoldBaseBranches(baseBranches: string[]): string[] { + const unfoldedList: string[] = []; + + const allBranches = getBranchList(); + for (const baseBranch of baseBranches) { + const isAllowedPred = configRegexPredicate(baseBranch); + if (isAllowedPred) { + const matchingBranches = allBranches.filter(isAllowedPred); + unfoldedList.push(...matchingBranches); + } else { + unfoldedList.push(baseBranch); + } + } + + return [...new Set(unfoldedList)]; +} + export async function extractDependencies( config: RenovateConfig ): Promise<ExtractResult> { @@ -91,6 +109,7 @@ export async function extractDependencies( packageFiles: null!, }; if (config.baseBranches?.length) { + config.baseBranches = unfoldBaseBranches(config.baseBranches); logger.debug({ baseBranches: config.baseBranches }, 'baseBranches'); const extracted: Record<string, Record<string, PackageFile[]>> = {}; for (const baseBranch of config.baseBranches) {