diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md index 093658d1432bf3..a0b519f6113340 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/`: `"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\\/.*/"] +} +``` + !!! 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 7acc420308e7e9..72a19f22ba1163 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 3574e7619ce107..fa2d0c2a6041c7 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 b98fae84d9f679..52d7e9a1deda0a 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 0e45a6058e78b6..8573c31713e8a4 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 b1cd6d8ae49304..c4c5357895bc1a 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 { @@ -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> = {}; for (const baseBranch of config.baseBranches) {