From c85932d8d3610b1e685b980d149db02aaf1fc881 Mon Sep 17 00:00:00 2001 From: Adam Setch <adam.setch@outlook.com> Date: Fri, 21 Jul 2023 00:21:36 -0400 Subject: [PATCH] feat(package-rules): add matchRepositories / excludeRepositories (#23085) Co-authored-by: Rhys Arkins <rhys@arkins.net> --- docs/usage/configuration-options.md | 34 ++++ lib/config/options/index.ts | 26 +++ lib/config/types.ts | 3 + lib/config/validation.ts | 2 + lib/util/package-rules/match.ts | 30 +++ lib/util/package-rules/matchers.ts | 2 + lib/util/package-rules/repositories.spec.ts | 201 ++++++++++++++++++++ lib/util/package-rules/repositories.ts | 19 ++ 8 files changed, 317 insertions(+) create mode 100644 lib/util/package-rules/match.ts create mode 100644 lib/util/package-rules/repositories.spec.ts create mode 100644 lib/util/package-rules/repositories.ts diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md index 81e333292e..1afdd2c5a5 100644 --- a/docs/usage/configuration-options.md +++ b/docs/usage/configuration-options.md @@ -2045,6 +2045,23 @@ See also `matchPackagePrefixes`. The above will match all package names starting with `eslint` but exclude ones starting with `eslint-foo`. +### excludeRepositories + +Use this field to restrict rules to a particular repository. e.g. + +```json +{ + "packageRules": [ + { + "excludeRepositories": ["literal/repo", "/^some/.*$/", "**/*-archived"], + "enabled": false + } + ] +} +``` + +This field supports Regular Expressions if they begin and end with `/`, otherwise it will use `minimatch`. + ### matchCategories Use `matchCategories` to restrict rules to a particular language or group. @@ -2067,6 +2084,23 @@ The categories can be found in the [manager documentation](./modules/manager/ind } ``` +### matchRepositories + +Use this field to restrict rules to a particular repository. e.g. + +```json +{ + "packageRules": [ + { + "matchRepositories": ["literal/repo", "/^some/.*$/", "**/*-archived"], + "enabled": false + } + ] +} +``` + +This field supports Regular Expressions if they begin and end with `/`, otherwise it will use `minimatch`. + ### matchBaseBranches Use this field to restrict rules to a particular branch. e.g. diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts index 1c419fe53c..017c200e2a 100644 --- a/lib/config/options/index.ts +++ b/lib/config/options/index.ts @@ -1042,6 +1042,32 @@ const options: RenovateOptions[] = [ cli: false, env: false, }, + { + name: 'matchRepositories', + description: + 'List of repositories to match (e.g. `["**/*-archived"]`). Valid only within a `packageRules` object.', + type: 'array', + subType: 'string', + allowString: true, + stage: 'package', + parent: 'packageRules', + mergeable: true, + cli: false, + env: false, + }, + { + name: 'excludeRepositories', + description: + 'List of repositories to exclude (e.g. `["**/*-archived"]`). Valid only within a `packageRules` object.', + type: 'array', + subType: 'string', + allowString: true, + stage: 'package', + parent: 'packageRules', + mergeable: true, + cli: false, + env: false, + }, { name: 'matchBaseBranches', description: diff --git a/lib/config/types.ts b/lib/config/types.ts index 6b515293d9..027f00952a 100644 --- a/lib/config/types.ts +++ b/lib/config/types.ts @@ -338,11 +338,13 @@ export interface PackageRule matchPackageNames?: string[]; matchPackagePatterns?: string[]; matchPackagePrefixes?: string[]; + matchRepositories?: string[]; excludeDepNames?: string[]; excludeDepPatterns?: string[]; excludePackageNames?: string[]; excludePackagePatterns?: string[]; excludePackagePrefixes?: string[]; + excludeRepositories?: string[]; matchCurrentValue?: string; matchCurrentVersion?: string; matchSourceUrlPrefixes?: string[]; @@ -495,6 +497,7 @@ export interface PackageRuleInputConfig extends Record<string, unknown> { manager?: string; datasource?: string; packageRules?: (PackageRule & PackageRuleInputConfig)[]; + repository?: string; } export interface ConfigMigration { diff --git a/lib/config/validation.ts b/lib/config/validation.ts index da0fbaba47..fded1fe4f2 100644 --- a/lib/config/validation.ts +++ b/lib/config/validation.ts @@ -319,12 +319,14 @@ export async function validateConfig( 'excludePackageNames', 'excludePackagePatterns', 'excludePackagePrefixes', + 'excludeRepositories', 'matchCurrentValue', 'matchCurrentVersion', 'matchSourceUrlPrefixes', 'matchSourceUrls', 'matchUpdateTypes', 'matchConfidence', + 'matchRepositories', ]; if (key === 'packageRules') { for (const [subIndex, packageRule] of val.entries()) { diff --git a/lib/util/package-rules/match.ts b/lib/util/package-rules/match.ts new file mode 100644 index 0000000000..3ae80396ed --- /dev/null +++ b/lib/util/package-rules/match.ts @@ -0,0 +1,30 @@ +import is from '@sindresorhus/is'; +import { minimatch } from 'minimatch'; +import { logger } from '../../logger'; +import { regEx } from '../regex'; + +export function matchRegexOrMinimatch(pattern: string, input: string): boolean { + if (pattern.length > 2 && pattern.startsWith('/') && pattern.endsWith('/')) { + try { + const regex = regEx(pattern.slice(1, -1)); + return regex.test(input); + } catch (err) { + logger.once.warn({ err, pattern }, 'Invalid regex pattern'); + return false; + } + } + return minimatch(input, pattern, { dot: true }); +} + +export function anyMatchRegexOrMinimatch( + patterns: string[] | undefined, + input: string | undefined +): boolean | null { + if (is.undefined(patterns)) { + return null; + } + if (is.undefined(input)) { + return false; + } + return patterns.some((pattern) => matchRegexOrMinimatch(pattern, input)); +} diff --git a/lib/util/package-rules/matchers.ts b/lib/util/package-rules/matchers.ts index 7d8856923e..78c6ab1c2b 100644 --- a/lib/util/package-rules/matchers.ts +++ b/lib/util/package-rules/matchers.ts @@ -12,6 +12,7 @@ import { MergeConfidenceMatcher } from './merge-confidence'; import { PackageNameMatcher } from './package-names'; import { PackagePatternsMatcher } from './package-patterns'; import { PackagePrefixesMatcher } from './package-prefixes'; +import { RepositoriesMatcher } from './repositories'; import { SourceUrlPrefixesMatcher } from './sourceurl-prefixes'; import { SourceUrlsMatcher } from './sourceurls'; import type { MatcherApi } from './types'; @@ -42,4 +43,5 @@ matchers.push([new UpdateTypesMatcher()]); matchers.push([new SourceUrlsMatcher(), new SourceUrlPrefixesMatcher()]); matchers.push([new CurrentValueMatcher()]); matchers.push([new CurrentVersionMatcher()]); +matchers.push([new RepositoriesMatcher()]); matchers.push([new CategoriesMatcher()]); diff --git a/lib/util/package-rules/repositories.spec.ts b/lib/util/package-rules/repositories.spec.ts new file mode 100644 index 0000000000..76a2e41dca --- /dev/null +++ b/lib/util/package-rules/repositories.spec.ts @@ -0,0 +1,201 @@ +import { RepositoriesMatcher } from './repositories'; + +describe('util/package-rules/repositories', () => { + const packageNameMatcher = new RepositoriesMatcher(); + + describe('match', () => { + it('should return null if match repositories is not defined', () => { + const result = packageNameMatcher.matches( + { + repository: 'org/repo', + }, + { + matchRepositories: undefined, + } + ); + expect(result).toBeNull(); + }); + + it('should return false if repository is not defined', () => { + const result = packageNameMatcher.matches( + { + repository: undefined, + }, + { + matchRepositories: ['org/repo'], + } + ); + expect(result).toBeFalse(); + }); + + it('should return true if repository matches regex pattern', () => { + const result = packageNameMatcher.matches( + { + repository: 'org/repo', + }, + { + matchRepositories: ['/^org/repo$/'], + } + ); + expect(result).toBeTrue(); + }); + + it('should return false if repository has invalid regex pattern', () => { + const result = packageNameMatcher.matches( + { + repository: 'org/repo', + }, + { + matchRepositories: ['/[/'], + } + ); + expect(result).toBeFalse(); + }); + + it('should return false if repository does not match regex pattern', () => { + const result = packageNameMatcher.matches( + { + repository: 'org/repo', + }, + { + matchRepositories: ['/^org/other-repo$/'], + } + ); + expect(result).toBeFalse(); + }); + + it('should return true if repository matches minimatch pattern', () => { + const result = packageNameMatcher.matches( + { + repository: 'org/repo', + }, + { + matchRepositories: ['org/**'], + } + ); + expect(result).toBeTrue(); + }); + + it('should return false if repository does not match minimatch pattern', () => { + const result = packageNameMatcher.matches( + { + repository: 'org/repo', + }, + { + matchRepositories: ['other-org/**'], + } + ); + expect(result).toBeFalse(); + }); + + it('should return true if repository matches at least one pattern', () => { + const result = packageNameMatcher.matches( + { + repository: 'org/repo-archived', + }, + { + matchRepositories: ['/^org/repo$/', '**/*-archived'], + } + ); + expect(result).toBeTrue(); + }); + }); + + describe('excludes', () => { + it('should return null if exclude repositories is not defined', () => { + const result = packageNameMatcher.excludes( + { + repository: 'org/repo', + }, + { + excludeRepositories: undefined, + } + ); + expect(result).toBeNull(); + }); + + it('should return false if exclude repository is not defined', () => { + const result = packageNameMatcher.excludes( + { + repository: undefined, + }, + { + excludeRepositories: ['org/repo'], + } + ); + expect(result).toBeFalse(); + }); + + it('should return true if exclude repository matches regex pattern', () => { + const result = packageNameMatcher.excludes( + { + repository: 'org/repo', + }, + { + excludeRepositories: ['/^org/repo$/'], + } + ); + expect(result).toBeTrue(); + }); + + it('should return false if exclude repository has invalid regex pattern', () => { + const result = packageNameMatcher.excludes( + { + repository: 'org/repo', + }, + { + excludeRepositories: ['/[/'], + } + ); + expect(result).toBeFalse(); + }); + + it('should return false if exclude repository does not match regex pattern', () => { + const result = packageNameMatcher.excludes( + { + repository: 'org/repo', + }, + { + excludeRepositories: ['/^org/other-repo$/'], + } + ); + expect(result).toBeFalse(); + }); + + it('should return true if exclude repository matches minimatch pattern', () => { + const result = packageNameMatcher.excludes( + { + repository: 'org/repo', + }, + { + excludeRepositories: ['org/**'], + } + ); + expect(result).toBeTrue(); + }); + + it('should return false if exclude repository does not match minimatch pattern', () => { + const result = packageNameMatcher.excludes( + { + repository: 'org/repo', + }, + { + excludeRepositories: ['other-org/**'], + } + ); + expect(result).toBeFalse(); + }); + + it('should return true if exclude repository matches at least one pattern', () => { + const result = packageNameMatcher.excludes( + { + repository: 'org/repo-archived', + }, + { + excludeRepositories: ['/^org/repo$/', '**/*-archived'], + } + ); + expect(result).toBeTrue(); + }); + }); +}); diff --git a/lib/util/package-rules/repositories.ts b/lib/util/package-rules/repositories.ts new file mode 100644 index 0000000000..8e405d87c3 --- /dev/null +++ b/lib/util/package-rules/repositories.ts @@ -0,0 +1,19 @@ +import type { PackageRule, PackageRuleInputConfig } from '../../config/types'; +import { Matcher } from './base'; +import { anyMatchRegexOrMinimatch } from './match'; + +export class RepositoriesMatcher extends Matcher { + override matches( + { repository }: PackageRuleInputConfig, + { matchRepositories }: PackageRule + ): boolean | null { + return anyMatchRegexOrMinimatch(matchRepositories, repository); + } + + override excludes( + { repository }: PackageRuleInputConfig, + { excludeRepositories }: PackageRule + ): boolean | null { + return anyMatchRegexOrMinimatch(excludeRepositories, repository); + } +} -- GitLab