diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md index 9fd548a998c7d6..d3bddbf4ebf364 100644 --- a/docs/usage/configuration-options.md +++ b/docs/usage/configuration-options.md @@ -2510,6 +2510,8 @@ Invalid if used outside of a `packageRule`. ### excludeDepPatterns +### excludeDepPrefixes + ### excludePackageNames **Important**: Do not mix this up with the option `ignoreDeps`. @@ -2830,6 +2832,8 @@ It is recommended that you avoid using "negative" globs, like `**/!(package.json ### matchDepPatterns +### matchDepPrefixes + ### matchNewValue This option is matched against the `newValue` field of a dependency. diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts index d40a1ed85abe1f..9601bca00b0502 100644 --- a/lib/config/options/index.ts +++ b/lib/config/options/index.ts @@ -1375,6 +1375,34 @@ const options: RenovateOptions[] = [ cli: false, env: false, }, + { + name: 'matchDepPrefixes', + description: + 'Dep names prefixes to match. Valid only within a `packageRules` object.', + type: 'array', + subType: 'string', + allowString: true, + stage: 'package', + parents: ['packageRules'], + mergeable: true, + cli: false, + env: false, + advancedUse: true, + }, + { + name: 'excludeDepPrefixes', + description: + 'Dep names prefixes to exclude. Valid only within a `packageRules` object.', + type: 'array', + subType: 'string', + allowString: true, + stage: 'package', + parents: ['packageRules'], + mergeable: true, + cli: false, + env: false, + advancedUse: true, + }, { name: 'matchPackagePatterns', description: diff --git a/lib/config/types.ts b/lib/config/types.ts index 6c68f35093a84a..2e55575c98927a 100644 --- a/lib/config/types.ts +++ b/lib/config/types.ts @@ -353,6 +353,7 @@ export interface PackageRule description?: string | string[]; excludeDepNames?: string[]; excludeDepPatterns?: string[]; + excludeDepPrefixes?: string[]; excludePackageNames?: string[]; excludePackagePatterns?: string[]; excludePackagePrefixes?: string[]; @@ -367,6 +368,7 @@ export interface PackageRule matchDatasources?: string[]; matchDepNames?: string[]; matchDepPatterns?: string[]; + matchDepPrefixes?: string[]; matchDepTypes?: string[]; matchFileNames?: string[]; matchManagers?: string[]; diff --git a/lib/config/validation.ts b/lib/config/validation.ts index 4ad370482ce52a..e12b7fb67bf816 100644 --- a/lib/config/validation.ts +++ b/lib/config/validation.ts @@ -396,11 +396,13 @@ export async function validateConfig( 'matchDepTypes', 'matchDepNames', 'matchDepPatterns', + 'matchDepPrefixes', 'matchPackageNames', 'matchPackagePatterns', 'matchPackagePrefixes', 'excludeDepNames', 'excludeDepPatterns', + 'excludeDepPrefixes', 'excludePackageNames', 'excludePackagePatterns', 'excludePackagePrefixes', diff --git a/lib/util/package-rules/dep-prefixes.spec.ts b/lib/util/package-rules/dep-prefixes.spec.ts new file mode 100644 index 00000000000000..d5b35da075989d --- /dev/null +++ b/lib/util/package-rules/dep-prefixes.spec.ts @@ -0,0 +1,105 @@ +import { DepPrefixesMatcher } from './dep-prefixes'; + +describe('util/package-rules/dep-prefixes', () => { + const depPrefixesMatcher = new DepPrefixesMatcher(); + + describe('match', () => { + it('should return null if matchDepPrefixes is not defined', () => { + const result = depPrefixesMatcher.matches( + { + depName: 'abc1', + }, + { + matchDepPrefixes: undefined, + }, + ); + expect(result).toBeNull(); + }); + + it('should return false if depName is not defined', () => { + const result = depPrefixesMatcher.matches( + { + depName: undefined, + }, + { + matchDepPrefixes: ['@opentelemetry'], + }, + ); + expect(result).toBeFalse(); + }); + + it('should return true if depName matched', () => { + const result = depPrefixesMatcher.matches( + { + depName: 'abc1', + }, + { + matchDepPrefixes: ['abc'], + }, + ); + expect(result).toBeTrue(); + }); + + it('should return false if depName does not match', () => { + const result = depPrefixesMatcher.matches( + { + depName: 'abc1', + }, + { + matchDepPrefixes: ['def'], + }, + ); + expect(result).toBeFalse(); + }); + }); + + describe('exclude', () => { + it('should return null if excludeDepPrefixes is not defined', () => { + const result = depPrefixesMatcher.excludes( + { + depName: 'abc1', + }, + { + excludeDepPrefixes: undefined, + }, + ); + expect(result).toBeNull(); + }); + + it('should return false if depName is not defined', () => { + const result = depPrefixesMatcher.excludes( + { + depName: undefined, + }, + { + excludeDepPrefixes: ['@opentelemetry'], + }, + ); + expect(result).toBeFalse(); + }); + + it('should return true if depName matched', () => { + const result = depPrefixesMatcher.excludes( + { + depName: 'abc1', + }, + { + excludeDepPrefixes: ['abc'], + }, + ); + expect(result).toBeTrue(); + }); + + it('should return false if depName does not match', () => { + const result = depPrefixesMatcher.excludes( + { + depName: 'abc1', + }, + { + excludeDepPrefixes: ['def'], + }, + ); + expect(result).toBeFalse(); + }); + }); +}); diff --git a/lib/util/package-rules/dep-prefixes.ts b/lib/util/package-rules/dep-prefixes.ts new file mode 100644 index 00000000000000..351df41f8605e5 --- /dev/null +++ b/lib/util/package-rules/dep-prefixes.ts @@ -0,0 +1,35 @@ +import is from '@sindresorhus/is'; +import type { PackageRule, PackageRuleInputConfig } from '../../config/types'; +import { Matcher } from './base'; + +export class DepPrefixesMatcher extends Matcher { + override matches( + { depName }: PackageRuleInputConfig, + { matchDepPrefixes }: PackageRule, + ): boolean | null { + if (is.undefined(matchDepPrefixes)) { + return null; + } + + if (is.undefined(depName)) { + return false; + } + + return matchDepPrefixes.some((prefix) => depName.startsWith(prefix)); + } + + override excludes( + { depName }: PackageRuleInputConfig, + { excludeDepPrefixes }: PackageRule, + ): boolean | null { + if (is.undefined(excludeDepPrefixes)) { + return null; + } + + if (is.undefined(depName)) { + return false; + } + + return excludeDepPrefixes.some((prefix) => depName.startsWith(prefix)); + } +} diff --git a/lib/util/package-rules/index.spec.ts b/lib/util/package-rules/index.spec.ts index 80e5716ee7a9ce..6a6a7dc01407ba 100644 --- a/lib/util/package-rules/index.spec.ts +++ b/lib/util/package-rules/index.spec.ts @@ -1230,4 +1230,52 @@ describe('util/package-rules/index', () => { expect(res1.x).toBeUndefined(); expect(res2.x).toBe(1); }); + + it('matches matchDepPrefixes(depName)', () => { + const config: TestConfig = { + packageRules: [ + { + matchDepPrefixes: ['abc'], + x: 1, + }, + ], + }; + + const res1 = applyPackageRules({ + ...config, + depName: 'abc1', + }); + const res2 = applyPackageRules({ + ...config, + depName: 'def1', + }); + applyPackageRules(config); // coverage + + expect(res1.x).toBe(1); + expect(res2.x).toBeUndefined(); + }); + + it('matches excludeDepPrefixes(depName)', () => { + const config: TestConfig = { + packageRules: [ + { + excludeDepPrefixes: ['abc'], + x: 1, + }, + ], + }; + + const res1 = applyPackageRules({ + ...config, + depName: 'abc1', + }); + const res2 = applyPackageRules({ + ...config, + depName: 'def1', + }); + applyPackageRules(config); // coverage + + expect(res1.x).toBeUndefined(); + expect(res2.x).toBe(1); + }); }); diff --git a/lib/util/package-rules/matchers.ts b/lib/util/package-rules/matchers.ts index 4bef5c5784eb5e..80a0d19ff7c171 100644 --- a/lib/util/package-rules/matchers.ts +++ b/lib/util/package-rules/matchers.ts @@ -6,6 +6,7 @@ import { CurrentVersionMatcher } from './current-version'; import { DatasourcesMatcher } from './datasources'; import { DepNameMatcher } from './dep-names'; import { DepPatternsMatcher } from './dep-patterns'; +import { DepPrefixesMatcher } from './dep-prefixes'; import { DepTypesMatcher } from './dep-types'; import { FileNamesMatcher } from './files'; import { ManagersMatcher } from './managers'; @@ -32,6 +33,7 @@ matchers.push([new MergeConfidenceMatcher()]); matchers.push([ new DepNameMatcher(), new DepPatternsMatcher(), + new DepPrefixesMatcher(), new PackageNameMatcher(), new PackagePatternsMatcher(), new PackagePrefixesMatcher(),