diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md index c8eb2d5793646783d468afad684516ef4148a641..cc5194f6786fcaaa2985179b8522fd4ca1ffb6f9 100644 --- a/docs/usage/configuration-options.md +++ b/docs/usage/configuration-options.md @@ -1756,6 +1756,21 @@ Here's an example of where you use this to group together all packages from the } ``` +### matchSourceUrls + +Here's an example of where you use this to match exact package urls: + +```json +{ + "packageRules": [ + { + "matchSourceUrls": ["https://github.com/facebook/react"], + "groupName": "React" + } + ] +} +``` + ### matchUpdateTypes Use this field to match rules against types of updates. diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts index cdbb0ae6d4a62b408f334fad19a2a92fcc1ce735..a28240c12eea6e4650064bfac0223e9201843c0e 100644 --- a/lib/config/options/index.ts +++ b/lib/config/options/index.ts @@ -1022,6 +1022,18 @@ const options: RenovateOptions[] = [ cli: false, env: false, }, + { + name: 'matchSourceUrls', + description: 'A list of source URLs to exact match against.', + type: 'array', + subType: 'string', + allowString: true, + stage: 'package', + parent: 'packageRules', + mergeable: true, + cli: false, + env: false, + }, { name: 'replacementName', description: diff --git a/lib/config/types.ts b/lib/config/types.ts index 3bf10bab7ef118466424d39769aa3c38e59608a6..8c5a068ac3a6539c9ff31e5444bdef37042474a6 100644 --- a/lib/config/types.ts +++ b/lib/config/types.ts @@ -274,6 +274,7 @@ export interface PackageRule excludePackagePrefixes?: string[]; matchCurrentVersion?: string | Range; matchSourceUrlPrefixes?: string[]; + matchSourceUrls?: string[]; matchUpdateTypes?: UpdateType[]; } diff --git a/lib/config/validation.ts b/lib/config/validation.ts index 40e40ab33f65650db00a0ff3befa908fbf61e91d..383790945cb8ac05ffa68454b8e9a75ec69b0444 100644 --- a/lib/config/validation.ts +++ b/lib/config/validation.ts @@ -304,6 +304,7 @@ export async function validateConfig( 'excludePackagePrefixes', 'matchCurrentVersion', 'matchSourceUrlPrefixes', + 'matchSourceUrls', 'matchUpdateTypes', ]; if (key === 'packageRules') { diff --git a/lib/util/package-rules.spec.ts b/lib/util/package-rules.spec.ts index 30816b299aa75e44d50b208bf1525c03fe92ff17..ffb02321bff85f53b77ee5eb81b81ea12183423a 100644 --- a/lib/util/package-rules.spec.ts +++ b/lib/util/package-rules.spec.ts @@ -495,6 +495,68 @@ describe('util/package-rules', () => { const res = applyPackageRules({ ...config, ...dep }); expect(res.x).toBeUndefined(); }); + it('matches matchSourceUrls', () => { + const config: TestConfig = { + packageRules: [ + { + matchSourceUrls: [ + 'https://github.com/foo/bar', + 'https://github.com/renovatebot/presets', + ], + x: 1, + }, + ], + }; + const dep = { + depType: 'dependencies', + depName: 'a', + updateType: 'patch' as UpdateType, + sourceUrl: 'https://github.com/renovatebot/presets', + }; + const res = applyPackageRules({ ...config, ...dep }); + expect(res.x).toBe(1); + }); + it('non-matches matchSourceUrls', () => { + const config: TestConfig = { + packageRules: [ + { + matchSourceUrls: [ + 'https://github.com/foo/bar', + 'https://github.com/facebook/react', + ], + x: 1, + }, + ], + }; + const dep = { + depType: 'dependencies', + depName: 'a', + updateType: 'patch' as UpdateType, + sourceUrl: 'https://github.com/facebook/react-native', + }; + const res = applyPackageRules({ ...config, ...dep }); + expect(res.x).toBeUndefined(); + }); + it('handles matchSourceUrls when missing sourceUrl', () => { + const config: TestConfig = { + packageRules: [ + { + matchSourceUrls: [ + 'https://github.com/foo/bar', + 'https://github.com/renovatebot/', + ], + x: 1, + }, + ], + }; + const dep = { + depType: 'dependencies', + depName: 'a', + updateType: 'patch' as UpdateType, + }; + const res = applyPackageRules({ ...config, ...dep }); + expect(res.x).toBeUndefined(); + }); it('filters naked depType', () => { const config: TestConfig = { packageRules: [ @@ -791,4 +853,25 @@ describe('util/package-rules', () => { const res = applyPackageRules({ ...config, ...dep }); expect(res.x).toBe(1); }); + it('matches matchSourceUrls(case-insensitive)', () => { + const config: TestConfig = { + packageRules: [ + { + matchSourceUrls: [ + 'https://github.com/foo/bar', + 'https://github.com/Renovatebot/renovate', + ], + x: 1, + }, + ], + }; + const dep = { + depType: 'dependencies', + depName: 'a', + updateType: 'patch' as UpdateType, + sourceUrl: 'https://github.com/renovatebot/Renovate', + }; + const res = applyPackageRules({ ...config, ...dep }); + expect(res.x).toBe(1); + }); }); diff --git a/lib/util/package-rules.ts b/lib/util/package-rules.ts index 46a62b37cde52afe4a100b0ca9d309dd2b55524d..ec18e0f909ddcd078f670097f42470d1e48a6a1d 100644 --- a/lib/util/package-rules.ts +++ b/lib/util/package-rules.ts @@ -45,6 +45,7 @@ function matchesRule( const excludePackagePatterns = packageRule.excludePackagePatterns || []; const excludePackagePrefixes = packageRule.excludePackagePrefixes || []; const matchSourceUrlPrefixes = packageRule.matchSourceUrlPrefixes || []; + const matchSourceUrls = packageRule.matchSourceUrls || []; const matchCurrentVersion = packageRule.matchCurrentVersion || null; const matchUpdateTypes = packageRule.matchUpdateTypes || []; let positiveMatch = false; @@ -210,6 +211,16 @@ function matchesRule( } positiveMatch = true; } + if (matchSourceUrls.length) { + const upperCaseSourceUrl = sourceUrl?.toUpperCase(); + const isMatch = matchSourceUrls.some( + (url) => upperCaseSourceUrl === url.toUpperCase() + ); + if (!isMatch) { + return false; + } + positiveMatch = true; + } if (matchCurrentVersion) { const version = allVersioning.get(versioning); const matchCurrentVersionStr = matchCurrentVersion.toString();