diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md index 0014db7dbe2f4418a51969947ddabf7308ede13a..dfc7acf31b8cdd3e876e5313378e806475dbc893 100644 --- a/docs/usage/configuration-options.md +++ b/docs/usage/configuration-options.md @@ -1705,8 +1705,48 @@ Use this field to restrict rules to a particular datasource. e.g. } ``` +### matchCurrentValue + +This option is matched against the `currentValue` field of a dependency. + +`matchCurrentValue` 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"], + "matchCurrentValue": "/^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"], + "matchCurrentValue": "!/^0\\./" + } + ] +} +``` + ### matchCurrentVersion +The `currentVersion` field will be one of the following (in order of preference): + +- locked version if a lock file exists +- resolved version +- current value + +Consider using instead `matchCurrentValue` if you wish to match against the raw string value of a dependency. + `matchCurrentVersion` can be an exact SemVer version or a SemVer range: ```json diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts index 865d7a63b289e7dd41c069d2ea43b7af45dfbbaf..ad7c9eed569e4c7d614383991a96653ef0ca6caf 100644 --- a/lib/config/options/index.ts +++ b/lib/config/options/index.ts @@ -1078,6 +1078,17 @@ const options: RenovateOptions[] = [ cli: false, env: false, }, + { + name: 'matchCurrentValue', + description: + 'A regex to match against the raw currentValue string of a dependency. Valid only within a `packageRules` object.', + type: 'string', + stage: 'package', + parent: 'packageRules', + mergeable: true, + cli: false, + env: false, + }, { name: 'matchCurrentVersion', description: diff --git a/lib/config/types.ts b/lib/config/types.ts index 06d357c407d098c5b07d2d22ba1eeb1664af87fa..b7a97fb51dd0c9ce9b5f48b77c16123e378a6509 100644 --- a/lib/config/types.ts +++ b/lib/config/types.ts @@ -304,6 +304,7 @@ export interface PackageRule excludePackageNames?: string[]; excludePackagePatterns?: string[]; excludePackagePrefixes?: string[]; + matchCurrentValue?: string; matchCurrentVersion?: string | Range; matchSourceUrlPrefixes?: string[]; matchSourceUrls?: string[]; diff --git a/lib/config/validation.spec.ts b/lib/config/validation.spec.ts index 3838c79d69702c874f4dfa7af784242ea4364240..0191d6a42104fd1cdbe819327ff4d4854662e610 100644 --- a/lib/config/validation.spec.ts +++ b/lib/config/validation.spec.ts @@ -63,6 +63,30 @@ describe('config/validation', () => { expect(errors).toMatchSnapshot(); }); + it('catches invalid matchCurrentValue', async () => { + const config = { + packageRules: [ + { + matchPackageNames: ['foo'], + matchCurrentValue: '/^2/', + enabled: true, + }, + { + matchPackageNames: ['bar'], + matchCurrentValue: '^1', + enabled: true, + }, + { + matchPackageNames: ['quack'], + matchCurrentValue: '<1.0.0', + enabled: true, + }, + ], + }; + const { errors } = await configValidation.validateConfig(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 a9ba6d08d85c34f5c013be2585839adfe25b7b7a..11f89d717bf47fa7613fbbafcf322f9f3fc9f5c3 100644 --- a/lib/config/validation.ts +++ b/lib/config/validation.ts @@ -227,6 +227,15 @@ export async function validateConfig( message: `Invalid regExp for ${currentPath}: \`${val}\``, }); } + } else if ( + key === 'matchCurrentValue' && + is.string(val) && + !configRegexPredicate(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) { @@ -314,6 +323,7 @@ export async function validateConfig( 'excludePackageNames', 'excludePackagePatterns', 'excludePackagePrefixes', + 'matchCurrentValue', 'matchCurrentVersion', 'matchSourceUrlPrefixes', 'matchSourceUrls', @@ -504,7 +514,9 @@ export async function validateConfig( } } if ( - (selectors.includes(key) || key === 'matchCurrentVersion') && + (selectors.includes(key) || + key === 'matchCurrentVersion' || + key === 'matchCurrentValue') && // TODO: can be undefined ? #7154 !rulesRe.test(parentPath!) && // Inside a packageRule (parentPath || !isPreset) // top level in a preset diff --git a/lib/util/package-rules/current-value.spec.ts b/lib/util/package-rules/current-value.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..5fcb3585956f426f700f723feccca96dd44b4f8c --- /dev/null +++ b/lib/util/package-rules/current-value.spec.ts @@ -0,0 +1,53 @@ +import { CurrentValueMatcher } from './current-value'; + +describe('util/package-rules/current-value', () => { + const matcher = new CurrentValueMatcher(); + + describe('match', () => { + it('return null if non-regex', () => { + const result = matcher.matches( + { + currentValue: '"~> 1.1.0"', + }, + { + matchCurrentValue: '^v', + } + ); + expect(result).toBeFalse(); + }); + + it('return false for regex version non match', () => { + const result = matcher.matches( + { + currentValue: '"~> 1.1.0"', + }, + { + matchCurrentValue: '/^v/', + } + ); + expect(result).toBeFalse(); + }); + + it('return true for regex version match', () => { + const result = matcher.matches( + { + currentValue: '"~> 0.1.0"', + }, + { + matchCurrentValue: '/^"/', + } + ); + expect(result).toBeTrue(); + }); + + it('return false for now value', () => { + const result = matcher.matches( + {}, + { + matchCurrentValue: '/^v?[~ -]?0/', + } + ); + expect(result).toBeFalse(); + }); + }); +}); diff --git a/lib/util/package-rules/current-value.ts b/lib/util/package-rules/current-value.ts new file mode 100644 index 0000000000000000000000000000000000000000..c161a5149f03eb8dfd9104e8a50494103a17ad57 --- /dev/null +++ b/lib/util/package-rules/current-value.ts @@ -0,0 +1,31 @@ +import is from '@sindresorhus/is'; +import type { PackageRule, PackageRuleInputConfig } from '../../config/types'; +import { logger } from '../../logger'; +import { configRegexPredicate } from '../regex'; +import { Matcher } from './base'; + +export class CurrentValueMatcher extends Matcher { + override matches( + { currentValue }: PackageRuleInputConfig, + { matchCurrentValue }: PackageRule + ): boolean | null { + if (is.undefined(matchCurrentValue)) { + return null; + } + const matchCurrentValuePred = configRegexPredicate(matchCurrentValue); + + if (!matchCurrentValuePred) { + logger.debug( + { matchCurrentValue }, + 'matchCurrentValue should be a regex, starting and ending with `/`' + ); + return false; + } + + if (!currentValue) { + return false; + } + + return matchCurrentValuePred(currentValue); + } +} diff --git a/lib/util/package-rules/index.ts b/lib/util/package-rules/index.ts index 2817b9a582b4fb143ce7ad2aeec4943180b1786e..43c8acfcd71a32c58de52a0e16a8a32c79a10361 100644 --- a/lib/util/package-rules/index.ts +++ b/lib/util/package-rules/index.ts @@ -93,6 +93,7 @@ export function applyPackageRules<T extends PackageRuleInputConfig>( delete config.excludePackagePatterns; delete config.excludePackagePrefixes; delete config.matchDepTypes; + delete config.matchCurrentValue; delete config.matchCurrentVersion; } } diff --git a/lib/util/package-rules/matchers.ts b/lib/util/package-rules/matchers.ts index d0b0da1c57f68a7d1795f361b746cabd78e493aa..65a374c5dcb31cfed5816afb1dcad368a53ce822 100644 --- a/lib/util/package-rules/matchers.ts +++ b/lib/util/package-rules/matchers.ts @@ -1,4 +1,5 @@ import { BaseBranchesMatcher } from './base-branches'; +import { CurrentValueMatcher } from './current-value'; import { CurrentVersionMatcher } from './current-version'; import { DatasourcesMatcher } from './datasources'; import { DepTypesMatcher } from './dep-types'; @@ -32,4 +33,5 @@ matchers.push([new ManagersMatcher()]); matchers.push([new DatasourcesMatcher()]); matchers.push([new UpdateTypesMatcher()]); matchers.push([new SourceUrlsMatcher(), new SourceUrlPrefixesMatcher()]); +matchers.push([new CurrentValueMatcher()]); matchers.push([new CurrentVersionMatcher()]);