From 7e7124ef9313d4b08c39a023dfc920f630fd09d6 Mon Sep 17 00:00:00 2001 From: RahulGautamSingh <rahultesnik@gmail.com> Date: Wed, 28 Feb 2024 01:28:33 +0545 Subject: [PATCH] feat(package-rules): matchNewValue (#27374) --- docs/usage/configuration-options.md | 34 +++++++++++++ lib/config/options/index.ts | 11 ++++ lib/config/types.ts | 2 + lib/config/validation.spec.ts | 29 +++++++++++ lib/config/validation.ts | 10 ++++ lib/util/package-rules/matchers.ts | 2 + lib/util/package-rules/new-value.spec.ts | 65 ++++++++++++++++++++++++ lib/util/package-rules/new-value.ts | 31 +++++++++++ 8 files changed, 184 insertions(+) create mode 100644 lib/util/package-rules/new-value.spec.ts create mode 100644 lib/util/package-rules/new-value.ts diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md index 9cb987176c..ce0c1d80b2 100644 --- a/docs/usage/configuration-options.md +++ b/docs/usage/configuration-options.md @@ -2821,6 +2821,40 @@ It is recommended that you avoid using "negative" globs, like `**/!(package.json ### matchDepPatterns +### matchNewValue + +This option is matched against the `newValue` field of a dependency. + +`matchNewValue` supports Regular Expressions which must begin and end with `/`. +For example, the following enforces that only `1.*` versions will be used: + +```json +{ + "packageRules": [ + { + "matchPackagePatterns": ["io.github.resilience4j"], + "matchNewValue": "/^1\\./" + } + ] +} +``` + +This field also supports a special negated regex syntax to ignore certain versions. +Use the syntax `!/ /` like this: + +```json +{ + "packageRules": [ + { + "matchPackagePatterns": ["io.github.resilience4j"], + "matchNewValue": "!/^0\\./" + } + ] +} +``` + +For more details on this syntax see Renovate's [string pattern matching documentation](./string-pattern-matching.md). + ### matchPackageNames Use this field if you want to have one or more exact name matches in your package rule. diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts index 5063e00c09..0ece410309 100644 --- a/lib/config/options/index.ts +++ b/lib/config/options/index.ts @@ -1374,6 +1374,17 @@ const options: RenovateOptions[] = [ cli: false, env: false, }, + { + name: 'matchNewValue', + description: + 'A regex to match against the raw `newValue` string of a dependency. Valid only within a `packageRules` object.', + type: 'string', + stage: 'package', + parents: ['packageRules'], + mergeable: true, + cli: false, + env: false, + }, { name: 'matchSourceUrlPrefixes', description: diff --git a/lib/config/types.ts b/lib/config/types.ts index da2d47b9c8..6e0715dabd 100644 --- a/lib/config/types.ts +++ b/lib/config/types.ts @@ -359,6 +359,7 @@ export interface PackageRule excludePackagePatterns?: string[]; excludePackagePrefixes?: string[]; excludeRepositories?: string[]; + matchNewValue?: string; matchCurrentValue?: string; matchCurrentVersion?: string; matchSourceUrlPrefixes?: string[]; @@ -498,6 +499,7 @@ export interface PackageRuleInputConfig extends Record<string, unknown> { depTypes?: string[]; depName?: string; packageName?: string | null; + newValue?: string | null; currentValue?: string | null; currentVersion?: string; lockedVersion?: string; diff --git a/lib/config/validation.spec.ts b/lib/config/validation.spec.ts index 70d5233a9d..485d4678e5 100644 --- a/lib/config/validation.spec.ts +++ b/lib/config/validation.spec.ts @@ -129,6 +129,35 @@ describe('config/validation', () => { expect(errors).toHaveLength(2); }); + it('catches invalid matchNewValue', async () => { + const config = { + packageRules: [ + { + matchPackageNames: ['foo'], + matchNewValue: '/^2/', + enabled: true, + }, + { + matchPackageNames: ['bar'], + matchNewValue: '^1', + enabled: true, + }, + { + matchPackageNames: ['quack'], + matchNewValue: '<1.0.0', + enabled: true, + }, + { + matchPackageNames: ['foo'], + matchNewValue: '/^2/i', + enabled: true, + }, + ], + }; + const { errors } = await configValidation.validateConfig(false, config); + expect(errors).toHaveLength(2); + }); + it('catches invalid matchCurrentVersion regex', async () => { const config = { packageRules: [ diff --git a/lib/config/validation.ts b/lib/config/validation.ts index 7b3ea7af65..61621457b6 100644 --- a/lib/config/validation.ts +++ b/lib/config/validation.ts @@ -285,6 +285,15 @@ export async function validateConfig( topic: 'Configuration Error', message: `Invalid regExp for ${currentPath}: \`${val}\``, }); + } else if ( + key === 'matchNewValue' && + is.string(val) && + !getRegexPredicate(val) + ) { + errors.push({ + topic: 'Configuration Error', + message: `Invalid regExp for ${currentPath}: \`${val}\``, + }); } else if (key === 'timezone' && val !== null) { const [validTimezone, errorMessage] = hasValidTimezone(val as string); if (!validTimezone) { @@ -386,6 +395,7 @@ export async function validateConfig( 'matchConfidence', 'matchCurrentAge', 'matchRepositories', + 'matchNewValue', ]; if (key === 'packageRules') { for (const [subIndex, packageRule] of val.entries()) { diff --git a/lib/util/package-rules/matchers.ts b/lib/util/package-rules/matchers.ts index 67cc39ec3b..4bef5c5784 100644 --- a/lib/util/package-rules/matchers.ts +++ b/lib/util/package-rules/matchers.ts @@ -10,6 +10,7 @@ import { DepTypesMatcher } from './dep-types'; import { FileNamesMatcher } from './files'; import { ManagersMatcher } from './managers'; import { MergeConfidenceMatcher } from './merge-confidence'; +import { NewValueMatcher } from './new-value'; import { PackageNameMatcher } from './package-names'; import { PackagePatternsMatcher } from './package-patterns'; import { PackagePrefixesMatcher } from './package-prefixes'; @@ -43,6 +44,7 @@ matchers.push([new DatasourcesMatcher()]); matchers.push([new UpdateTypesMatcher()]); matchers.push([new SourceUrlsMatcher(), new SourceUrlPrefixesMatcher()]); matchers.push([new CurrentValueMatcher()]); +matchers.push([new NewValueMatcher()]); matchers.push([new CurrentVersionMatcher()]); matchers.push([new RepositoriesMatcher()]); matchers.push([new CategoriesMatcher()]); diff --git a/lib/util/package-rules/new-value.spec.ts b/lib/util/package-rules/new-value.spec.ts new file mode 100644 index 0000000000..323de64d54 --- /dev/null +++ b/lib/util/package-rules/new-value.spec.ts @@ -0,0 +1,65 @@ +import { NewValueMatcher } from './new-value'; + +describe('util/package-rules/new-value', () => { + const matcher = new NewValueMatcher(); + + describe('match', () => { + it('return null if non-regex', () => { + const result = matcher.matches( + { + newValue: '"~> 1.1.0"', + }, + { + matchNewValue: '^v', + }, + ); + expect(result).toBeFalse(); + }); + + it('return false for regex version non match', () => { + const result = matcher.matches( + { + newValue: '"~> 1.1.0"', + }, + { + matchNewValue: '/^v/', + }, + ); + expect(result).toBeFalse(); + }); + + it('case insensitive match', () => { + const result = matcher.matches( + { + newValue: '"V1.1.0"', + }, + { + matchNewValue: '/^"v/i', + }, + ); + expect(result).toBeTrue(); + }); + + it('return true for regex version match', () => { + const result = matcher.matches( + { + newValue: '"~> 0.1.0"', + }, + { + matchNewValue: '/^"/', + }, + ); + expect(result).toBeTrue(); + }); + + it('return false for now value', () => { + const result = matcher.matches( + {}, + { + matchNewValue: '/^v?[~ -]?0/', + }, + ); + expect(result).toBeFalse(); + }); + }); +}); diff --git a/lib/util/package-rules/new-value.ts b/lib/util/package-rules/new-value.ts new file mode 100644 index 0000000000..b8c28f5f82 --- /dev/null +++ b/lib/util/package-rules/new-value.ts @@ -0,0 +1,31 @@ +import is from '@sindresorhus/is'; +import type { PackageRule, PackageRuleInputConfig } from '../../config/types'; +import { logger } from '../../logger'; +import { getRegexPredicate } from '../string-match'; +import { Matcher } from './base'; + +export class NewValueMatcher extends Matcher { + override matches( + { newValue }: PackageRuleInputConfig, + { matchNewValue }: PackageRule, + ): boolean | null { + if (is.undefined(matchNewValue)) { + return null; + } + const matchNewValuePred = getRegexPredicate(matchNewValue); + + if (!matchNewValuePred) { + logger.debug( + { matchNewValue }, + 'matchNewValue should be a regex, starting and ending with `/`', + ); + return false; + } + + if (!newValue) { + return false; + } + + return matchNewValuePred(newValue); + } +} -- GitLab