From d8ec6258c41c4f38016dbff0aba215dd50f42eff Mon Sep 17 00:00:00 2001 From: Rhys Arkins Date: Tue, 23 Apr 2024 15:47:42 +0200 Subject: [PATCH] feat(packageRules)!: support regex or glob matching for all (#28591) Co-authored-by: Michael Kriese --- docs/usage/configuration-options.md | 16 +++++++++++++ docs/usage/string-pattern-matching.md | 11 ++++----- lib/util/package-rules/base-branches.ts | 10 ++------ lib/util/package-rules/categories.ts | 3 ++- lib/util/package-rules/datasources.ts | 3 ++- lib/util/package-rules/dep-types.ts | 14 +++++++---- lib/util/package-rules/files.ts | 19 +++++++-------- lib/util/package-rules/index.spec.ts | 18 +++++++++++++++ lib/util/package-rules/managers.ts | 5 ++-- .../package-rules/package-patterns.spec.ts | 2 +- lib/util/package-rules/sourceurls.ts | 8 +++---- lib/util/package-rules/update-types.ts | 13 +++++++---- lib/util/string-match.spec.ts | 23 +++++++++++++++++++ lib/util/string-match.ts | 7 ++++++ 14 files changed, 111 insertions(+), 41 deletions(-) diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md index bf68640bbe1b23..82755b0b179fa3 100644 --- a/docs/usage/configuration-options.md +++ b/docs/usage/configuration-options.md @@ -2545,6 +2545,8 @@ Instead you should do `> 13 months`. Use this field if you want to limit a `packageRule` to certain `depType` values. Invalid if used outside of a `packageRule`. +For more details on supported syntax see Renovate's [string pattern matching documentation](./string-pattern-matching.md). + ### excludeDepNames ### excludeDepPatterns @@ -2647,6 +2649,8 @@ The categories can be found in the [manager documentation](modules/manager/index } ``` +For more details on supported syntax see Renovate's [string pattern matching documentation](./string-pattern-matching.md). + ### matchRepositories Use this field to restrict rules to a particular repository. e.g. @@ -2694,6 +2698,8 @@ This field also supports Regular Expressions if they begin and end with `/`. e.g } ``` +For more details on supported syntax see Renovate's [string pattern matching documentation](./string-pattern-matching.md). + ### matchManagers Use this field to restrict rules to a particular package manager. e.g. @@ -2712,6 +2718,8 @@ Use this field to restrict rules to a particular package manager. e.g. For the full list of available managers, see the [Supported Managers](modules/manager/index.md#supported-managers) documentation. +For more details on supported syntax see Renovate's [string pattern matching documentation](./string-pattern-matching.md). + ### matchMessage For log level remapping, use this field to match against the particular log messages. @@ -2733,6 +2741,8 @@ Use this field to restrict rules to a particular datasource. e.g. } ``` +For more details on supported syntax see Renovate's [string pattern matching documentation](./string-pattern-matching.md). + ### matchCurrentValue This option is matched against the `currentValue` field of a dependency. @@ -2867,6 +2877,8 @@ The following example matches any file in directories starting with `app/`: It is recommended that you avoid using "negative" globs, like `**/!(package.json)`, because such patterns might still return true if they match against the lock file name (e.g. `package-lock.json`). +For more details on supported syntax see Renovate's [string pattern matching documentation](./string-pattern-matching.md). + ### matchDepNames This field behaves the same as `matchPackageNames` except it matches against `depName` instead of `packageName`. @@ -3029,6 +3041,8 @@ Here's an example of where you use this to group together all packages from the } ``` +For more details on supported syntax see Renovate's [string pattern matching documentation](./string-pattern-matching.md). + ### matchUpdateTypes Use `matchUpdateTypes` to match rules against types of updates. @@ -3045,6 +3059,8 @@ For example to apply a special label to `major` updates: } ``` +For more details on supported syntax see Renovate's [string pattern matching documentation](./string-pattern-matching.md). + !!! warning Packages that follow SemVer are allowed to make breaking changes in _any_ `0.x` version, even `patch` and `minor`. diff --git a/docs/usage/string-pattern-matching.md b/docs/usage/string-pattern-matching.md index 02cbc6e3856f7d..c22001a58a4817 100644 --- a/docs/usage/string-pattern-matching.md +++ b/docs/usage/string-pattern-matching.md @@ -5,12 +5,7 @@ Renovate string matching syntax for some configuration options allows you, as us - [`minimatch`](https://github.com/isaacs/minimatch) glob patterns, including exact strings matches - regular expression (regex) patterns -The following fields support this pattern matching: - -- `allowedEnv` -- `allowedHeaders` -- `autodiscoverProjects` -- `matchRepositories` +In cases where there are potentially multiple _inputs_, e.g. managers can have multiple categories, then the matcher will return `true` if _any_ of them match. ## Special case: Match everything @@ -111,6 +106,10 @@ For example, the pattern `["/^abc/", "!/^abcd/", "!/abce/"]`: If you find yourself in a situation where you need to positive-match a string which starts with `!`, then you need to do so using a regular expression pattern. For example, `["/^!abc$/"]` will positively match against the string `"!abc"`. +One limitation of negative matching is when there may be multiple inputs to match against. +For example, a manager may have multiple categories, such as `java` and `docker`. +If you have a rule such as `"matchCategories": ["!docker"]` then this will return `true` because the `java` category satisfies this rule. + ## Usage in Renovate configuration options Renovate has evolved its approach to string pattern matching over time, but this means that existing configurations may have a mix of approaches and not be entirely consistent with each other. diff --git a/lib/util/package-rules/base-branches.ts b/lib/util/package-rules/base-branches.ts index 8e8cb2eb911941..71bf07b36408f3 100644 --- a/lib/util/package-rules/base-branches.ts +++ b/lib/util/package-rules/base-branches.ts @@ -1,6 +1,6 @@ import is from '@sindresorhus/is'; import type { PackageRule, PackageRuleInputConfig } from '../../config/types'; -import { getRegexPredicate } from '../string-match'; +import { matchRegexOrGlobList } from '../string-match'; import { Matcher } from './base'; export class BaseBranchesMatcher extends Matcher { @@ -16,12 +16,6 @@ export class BaseBranchesMatcher extends Matcher { return false; } - return matchBaseBranches.some((matchBaseBranch): boolean => { - const isAllowedPred = getRegexPredicate(matchBaseBranch); - if (isAllowedPred) { - return isAllowedPred(baseBranch); - } - return matchBaseBranch === baseBranch; - }); + return matchRegexOrGlobList(baseBranch, matchBaseBranches); } } diff --git a/lib/util/package-rules/categories.ts b/lib/util/package-rules/categories.ts index 0a5e47ae304580..b26a8e2ac80391 100644 --- a/lib/util/package-rules/categories.ts +++ b/lib/util/package-rules/categories.ts @@ -1,5 +1,6 @@ import is from '@sindresorhus/is'; import type { PackageRule, PackageRuleInputConfig } from '../../config/types'; +import { anyMatchRegexOrGlobList } from '../string-match'; import { Matcher } from './base'; export class CategoriesMatcher extends Matcher { @@ -15,6 +16,6 @@ export class CategoriesMatcher extends Matcher { return false; } - return matchCategories.some((value) => categories.includes(value)); + return anyMatchRegexOrGlobList(categories, matchCategories); } } diff --git a/lib/util/package-rules/datasources.ts b/lib/util/package-rules/datasources.ts index 3e6b5bef446e51..5fbfeca59481d6 100644 --- a/lib/util/package-rules/datasources.ts +++ b/lib/util/package-rules/datasources.ts @@ -1,5 +1,6 @@ import is from '@sindresorhus/is'; import type { PackageRule, PackageRuleInputConfig } from '../../config/types'; +import { matchRegexOrGlobList } from '../string-match'; import { Matcher } from './base'; export class DatasourcesMatcher extends Matcher { @@ -13,6 +14,6 @@ export class DatasourcesMatcher extends Matcher { if (is.undefined(datasource)) { return false; } - return matchDatasources.includes(datasource); + return matchRegexOrGlobList(datasource, matchDatasources); } } diff --git a/lib/util/package-rules/dep-types.ts b/lib/util/package-rules/dep-types.ts index 73a6087c96f8c2..434e81523dbefd 100644 --- a/lib/util/package-rules/dep-types.ts +++ b/lib/util/package-rules/dep-types.ts @@ -1,5 +1,6 @@ import is from '@sindresorhus/is'; import type { PackageRule, PackageRuleInputConfig } from '../../config/types'; +import { anyMatchRegexOrGlobList, matchRegexOrGlobList } from '../string-match'; import { Matcher } from './base'; export class DepTypesMatcher extends Matcher { @@ -11,9 +12,14 @@ export class DepTypesMatcher extends Matcher { return null; } - const result = - (is.string(depType) && matchDepTypes.includes(depType)) || - depTypes?.some((dt) => matchDepTypes.includes(dt)); - return result ?? false; + if (depType) { + return matchRegexOrGlobList(depType, matchDepTypes); + } + + if (depTypes) { + return anyMatchRegexOrGlobList(depTypes, matchDepTypes); + } + + return false; } } diff --git a/lib/util/package-rules/files.ts b/lib/util/package-rules/files.ts index 48b69c999181de..73151dbb07af80 100644 --- a/lib/util/package-rules/files.ts +++ b/lib/util/package-rules/files.ts @@ -1,6 +1,6 @@ import is from '@sindresorhus/is'; import type { PackageRule, PackageRuleInputConfig } from '../../config/types'; -import { minimatch } from '../minimatch'; +import { anyMatchRegexOrGlobList, matchRegexOrGlobList } from '../string-match'; import { Matcher } from './base'; export class FileNamesMatcher extends Matcher { @@ -15,13 +15,14 @@ export class FileNamesMatcher extends Matcher { return false; } - return matchFileNames.some( - (matchFileName) => - minimatch(matchFileName, { dot: true }).match(packageFile) || - (is.array(lockFiles) && - lockFiles.some((lockFile) => - minimatch(matchFileName, { dot: true }).match(lockFile), - )), - ); + if (matchRegexOrGlobList(packageFile, matchFileNames)) { + return true; + } + + if (is.array(lockFiles)) { + return anyMatchRegexOrGlobList(lockFiles, matchFileNames); + } + + return false; } } diff --git a/lib/util/package-rules/index.spec.ts b/lib/util/package-rules/index.spec.ts index 997941a7174863..9f911c3541fd9e 100644 --- a/lib/util/package-rules/index.spec.ts +++ b/lib/util/package-rules/index.spec.ts @@ -41,6 +41,7 @@ describe('util/package-rules/index', () => { it('applies', () => { const config: PackageRuleInputConfig = { packageName: 'a', + updateType: 'minor', isBump: true, currentValue: '1.0.0', packageRules: [ @@ -314,6 +315,23 @@ describe('util/package-rules/index', () => { expect(res.x).toBe(1); }); + it('returns false if no depTypes', () => { + const config: TestConfig = { + packageRules: [ + { + matchDepTypes: ['test'], + matchPackageNames: ['a'], + x: 1, + }, + ], + }; + const input = { ...config, packageName: 'a' }; + delete input.depType; + delete input.depTypes; + const res = applyPackageRules(input); + expect(res).toEqual(input); + }); + it('filters managers with matching manager', () => { const config: TestConfig = { packageRules: [ diff --git a/lib/util/package-rules/managers.ts b/lib/util/package-rules/managers.ts index 98c28ce198eeaf..487bcf3b0b0bcd 100644 --- a/lib/util/package-rules/managers.ts +++ b/lib/util/package-rules/managers.ts @@ -1,6 +1,7 @@ import is from '@sindresorhus/is'; import type { PackageRule, PackageRuleInputConfig } from '../../config/types'; import { isCustomManager } from '../../modules/manager/custom'; +import { matchRegexOrGlobList } from '../string-match'; import { Matcher } from './base'; export class ManagersMatcher extends Matcher { @@ -15,8 +16,8 @@ export class ManagersMatcher extends Matcher { return false; } if (isCustomManager(manager)) { - return matchManagers.includes(`custom.${manager}`); + return matchRegexOrGlobList(`custom.${manager}`, matchManagers); } - return matchManagers.includes(manager); + return matchRegexOrGlobList(manager, matchManagers); } } diff --git a/lib/util/package-rules/package-patterns.spec.ts b/lib/util/package-rules/package-patterns.spec.ts index 2d0cc223c98a47..b1933a8b793078 100644 --- a/lib/util/package-rules/package-patterns.spec.ts +++ b/lib/util/package-rules/package-patterns.spec.ts @@ -29,7 +29,7 @@ describe('util/package-rules/package-patterns', () => { }); it('should fall back to matching depName', () => { - const result = packageNameMatcher.matches( + const result = packagePatternsMatcher.matches( { depName: 'abc', packageName: 'def', diff --git a/lib/util/package-rules/sourceurls.ts b/lib/util/package-rules/sourceurls.ts index 0f1e311435490d..d1d9910a0f7911 100644 --- a/lib/util/package-rules/sourceurls.ts +++ b/lib/util/package-rules/sourceurls.ts @@ -1,5 +1,6 @@ import is from '@sindresorhus/is'; import type { PackageRule, PackageRuleInputConfig } from '../../config/types'; +import { matchRegexOrGlobList } from '../string-match'; import { Matcher } from './base'; export class SourceUrlsMatcher extends Matcher { @@ -10,13 +11,10 @@ export class SourceUrlsMatcher extends Matcher { if (is.undefined(matchSourceUrls)) { return null; } - if (is.undefined(sourceUrl)) { + if (!sourceUrl) { return false; } - const upperCaseSourceUrl = sourceUrl?.toUpperCase(); - return matchSourceUrls.some( - (url) => upperCaseSourceUrl === url.toUpperCase(), - ); + return matchRegexOrGlobList(sourceUrl, matchSourceUrls); } } diff --git a/lib/util/package-rules/update-types.ts b/lib/util/package-rules/update-types.ts index 85c5f3f750ea07..c54b26588ce9ee 100644 --- a/lib/util/package-rules/update-types.ts +++ b/lib/util/package-rules/update-types.ts @@ -1,5 +1,6 @@ import is from '@sindresorhus/is'; import type { PackageRule, PackageRuleInputConfig } from '../../config/types'; +import { anyMatchRegexOrGlobList } from '../string-match'; import { Matcher } from './base'; export class UpdateTypesMatcher extends Matcher { @@ -10,9 +11,13 @@ export class UpdateTypesMatcher extends Matcher { if (is.undefined(matchUpdateTypes)) { return null; } - return ( - (is.truthy(updateType) && matchUpdateTypes.includes(updateType)) || - (is.truthy(isBump) && matchUpdateTypes.includes('bump')) - ); + if (!updateType) { + return false; + } + const toMatch = [updateType]; + if (isBump) { + toMatch.push('bump'); + } + return anyMatchRegexOrGlobList(toMatch, matchUpdateTypes); } } diff --git a/lib/util/string-match.spec.ts b/lib/util/string-match.spec.ts index e76d200d7b03f5..626da73e184ee1 100644 --- a/lib/util/string-match.spec.ts +++ b/lib/util/string-match.spec.ts @@ -1,4 +1,5 @@ import { + anyMatchRegexOrGlobList, getRegexPredicate, matchRegexOrGlob, matchRegexOrGlobList, @@ -59,6 +60,28 @@ describe('util/string-match', () => { }); }); + describe('anyMatchRegexOrGlobList()', () => { + it('returns false if empty patterns', () => { + expect(anyMatchRegexOrGlobList(['test'], [])).toBeFalse(); + }); + + it('returns false if empty inputs', () => { + expect(anyMatchRegexOrGlobList([], ['/test2/'])).toBeFalse(); + }); + + it('returns true if both empty', () => { + expect(anyMatchRegexOrGlobList([], [])).toBeFalse(); + }); + + it('returns true if any match with positive', () => { + expect(anyMatchRegexOrGlobList(['a', 'b'], ['b'])).toBeTrue(); + }); + + it('returns true if any match with negative', () => { + expect(anyMatchRegexOrGlobList(['a', 'b'], ['!b'])).toBeTrue(); + }); + }); + describe('getRegexPredicate()', () => { it('allows valid regex pattern', () => { expect(getRegexPredicate('/hello/')).not.toBeNull(); diff --git a/lib/util/string-match.ts b/lib/util/string-match.ts index 80cbc6dc05d7bd..bb64c327515c86 100644 --- a/lib/util/string-match.ts +++ b/lib/util/string-match.ts @@ -59,6 +59,13 @@ export function matchRegexOrGlobList( return true; } +export function anyMatchRegexOrGlobList( + inputs: string[], + patterns: string[], +): boolean { + return inputs.some((input) => matchRegexOrGlobList(input, patterns)); +} + export const UUIDRegex = regEx( /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, );