feat!: merge matchPaths and matchFiles into matchFileNames (#22406)

Closes #22395

BREAKING CHANGE: matchPaths and matchFiles are now combined into matchFileNames, supporting exact match and glob-only
This commit is contained in:
Rhys Arkins 2023-05-25 17:46:28 +02:00
parent 033d776ab0
commit e3d5f7df92
23 changed files with 118 additions and 170 deletions

View file

@ -1872,28 +1872,28 @@ Example:
The above rule will group together the `neutrino` package and any package matching `@neutrino/*`.
Path rules are convenient to use if you wish to apply configuration rules to certain package files using patterns.
File name matches are convenient to use if you wish to apply configuration rules to certain package or lock files using patterns.
For example, if you have an `examples` directory and you want all updates to those examples to use the `chore` prefix instead of `fix`, then you could add this configuration:
```json
{
"packageRules": [
{
"matchPaths": ["examples/**"],
"matchFileNames": ["examples/**"],
"extends": [":semanticCommitTypeAll(chore)"]
}
]
}
```
If you wish to limit Renovate to apply configuration rules to certain files in the root repository directory, you have to use `matchPaths` with a `minimatch` pattern or use [`matchFiles`](#matchfiles) with an exact match.
If you wish to limit Renovate to apply configuration rules to certain files in the root repository directory, you have to use `matchFileNames` with a `minimatch` pattern (which can include an exact file name match).
For example you have multiple `package.json` and want to use `dependencyDashboardApproval` only on the root `package.json`:
```json
{
"packageRules": [
{
"matchFiles": ["package.json"],
"matchFileNames": ["package.json"],
"dependencyDashboardApproval": true
}
]
@ -2188,23 +2188,50 @@ Use the syntax `!/ /` like this:
}
```
### matchFiles
### matchFileNames
Renovate will compare `matchFiles` for an exact match against the dependency's package file or lock file.
Renovate will compare `matchFileNames` glob matching against the dependency's package file or lock file.
For example the following would match `package.json` but not `package/frontend/package.json`:
The following example matches `package.json` but _not_ `package/frontend/package.json`:
```json
{
"packageRules": [
{
"matchFiles": ["package.json"]
"matchFileNames": ["package.json"],
"labels": ["npm"]
}
]
}
```
Use [`matchPaths`](#matchpaths) instead if you need more flexible matching.
The following example matches any `package.json`, including files like `backend/package.json`:
```json
{
"packageRules": [
{
"description": "Group dependencies from package.json files",
"matchFileNames": ["**/package.json"],
"groupName": "All package.json changes"
}
]
}
```
The following example matches any file in directories starting with `app/`:
```json
{
"packageRules": [
{
"description": "Group all dependencies from the app directory",
"matchFileNames": ["app/**"],
"groupName": "App dependencies"
}
]
}
```
### matchDepNames
@ -2264,38 +2291,6 @@ See also `excludePackagePrefixes`.
Like the earlier `matchPackagePatterns` example, the above will configure `rangeStrategy` to `replace` for any package starting with `angular`.
### matchPaths
Renovate finds the file(s) listed in `matchPaths` with a `minimatch` glob pattern.
For example the following matches any `package.json`, including files like `backend/package.json`:
```json
{
"packageRules": [
{
"description": "Group dependencies from package.json files",
"matchPaths": ["**/package.json"],
"groupName": "All package.json changes"
}
]
}
```
The following matches any file in directories starting with `app/`:
```json
{
"packageRules": [
{
"description": "Group all dependencies from the app directory",
"matchPaths": ["app/**"],
"groupName": "App dependencies"
}
]
}
```
### matchSourceUrlPrefixes
Here's an example of where you use this to group together all packages from the `renovatebot` GitHub org:

View file

@ -164,7 +164,7 @@ exports[`config/migration migrateConfig(config, parentConfig) migrates config 1`
"extends": [
"node",
],
"matchPaths": [
"matchFileNames": [
"node/**",
],
},
@ -211,7 +211,7 @@ exports[`config/migration migrateConfig(config, parentConfig) migrates config 1`
"extends": [
"foo",
],
"matchPaths": [
"matchFileNames": [
"examples/**",
],
},
@ -293,7 +293,7 @@ exports[`config/migration migrateConfig(config, parentConfig) migrates more pack
"matchDepTypes": [
"devDependencies",
],
"matchPaths": [
"matchFileNames": [
"package.json",
],
"rangeStrategy": "pin",
@ -302,7 +302,7 @@ exports[`config/migration migrateConfig(config, parentConfig) migrates more pack
"matchDepTypes": [
"dependencies",
],
"matchPaths": [
"matchFileNames": [
"package.json",
],
"rangeStrategy": "pin",
@ -332,13 +332,13 @@ exports[`config/migration migrateConfig(config, parentConfig) migrates packageFi
],
"packageRules": [
{
"matchPaths": [
"matchFileNames": [
"backend/package.json",
],
"rangeStrategy": "replace",
},
{
"matchPaths": [
"matchFileNames": [
"frontend/package.json",
],
"rangeStrategy": "pin",
@ -347,7 +347,7 @@ exports[`config/migration migrateConfig(config, parentConfig) migrates packageFi
"matchDepTypes": [
"devDependencies",
],
"matchPaths": [
"matchFileNames": [
"other/package.json",
],
"rangeStrategy": "pin",
@ -356,7 +356,7 @@ exports[`config/migration migrateConfig(config, parentConfig) migrates packageFi
"matchDepTypes": [
"dependencies",
],
"matchPaths": [
"matchFileNames": [
"other/package.json",
],
"rangeStrategy": "pin",

View file

@ -568,13 +568,41 @@ describe('config/migration', () => {
matchManagers: ['dockerfile'],
matchPackageNames: ['foo'],
matchPackagePatterns: ['^bar'],
matchPaths: ['package.json'],
matchFileNames: ['package.json'],
matchSourceUrlPrefixes: ['https://github.com/lodash'],
matchUpdateTypes: ['major'],
},
],
});
});
it('migrates in order of precedence', () => {
const config: TestRenovateConfig = {
packageRules: [
{
matchFiles: ['matchFiles'],
matchPaths: ['matchPaths'],
},
{
matchPaths: ['matchPaths'],
matchFiles: ['matchFiles'],
},
],
};
const { isMigrated, migratedConfig } =
configMigration.migrateConfig(config);
expect(isMigrated).toBeTrue();
expect(migratedConfig).toEqual({
packageRules: [
{
matchFileNames: ['matchPaths'],
},
{
matchFileNames: ['matchFiles'],
},
],
});
});
});
it('it migrates nested packageRules', () => {

View file

@ -19,13 +19,13 @@ describe('config/migrations/custom/automerge-major-migration', () => {
{
automergeMajor: 'some-value',
major: {
matchFiles: ['test'],
matchFileNames: ['test'],
},
},
{
major: {
automerge: true,
matchFiles: ['test'],
matchFileNames: ['test'],
},
}
);

View file

@ -19,13 +19,13 @@ describe('config/migrations/custom/automerge-minor-migration', () => {
{
automergeMinor: 'some-value',
minor: {
matchFiles: ['test'],
matchFileNames: ['test'],
},
},
{
minor: {
automerge: true,
matchFiles: ['test'],
matchFileNames: ['test'],
},
}
);

View file

@ -19,13 +19,13 @@ describe('config/migrations/custom/automerge-patch-migration', () => {
{
automergePatch: 'some-value',
patch: {
matchFiles: ['test'],
matchFileNames: ['test'],
},
},
{
patch: {
automerge: true,
matchFiles: ['test'],
matchFileNames: ['test'],
},
}
);

View file

@ -48,7 +48,7 @@ describe('config/migrations/custom/package-rules-migration', () => {
{
packageRules: [
{
matchPaths: [],
matchFileNames: [],
packgageRules: {
languages: ['javascript'],
},

View file

@ -2,7 +2,9 @@ import type { PackageRule } from '../../types';
import { AbstractMigration } from '../base/abstract-migration';
export const renameMap = {
paths: 'matchPaths',
matchFiles: 'matchFileNames',
matchPaths: 'matchFileNames',
paths: 'matchFileNames',
languages: 'matchLanguages',
baseBranchList: 'matchBaseBranches',
managers: 'matchManagers',

View file

@ -1321,7 +1321,7 @@ const options: RenovateOptions[] = [
env: false,
},
{
name: 'matchFiles',
name: 'matchFileNames',
description:
'List of strings to do an exact match against package and lock files with full path. Only works inside a `packageRules` object.',
type: 'array',
@ -1331,17 +1331,6 @@ const options: RenovateOptions[] = [
cli: false,
env: false,
},
{
name: 'matchPaths',
description:
'List of glob patterns to match against package files. Only works inside a `packageRules` object.',
type: 'array',
subType: 'string',
stage: 'repository',
parent: 'packageRules',
cli: false,
env: false,
},
// Version behaviour
{
name: 'allowedVersions',

View file

@ -372,7 +372,7 @@ export const presets: Record<string, Preset> = {
'Use semanticCommitType `{{arg0}}` for all package files matching path `{{arg1}}`.',
packageRules: [
{
matchPaths: ['{{arg0}}'],
matchFileNames: ['{{arg0}}'],
semanticCommitType: '{{arg1}}',
},
],

View file

@ -318,8 +318,7 @@ export interface PackageRule
Record<string, unknown> {
description?: string | string[];
isVulnerabilityAlert?: boolean;
matchFiles?: string[];
matchPaths?: string[];
matchFileNames?: string[];
matchLanguages?: string[];
matchBaseBranches?: string[];
matchManagers?: string | string[];
@ -464,6 +463,7 @@ export type RenovateOptions =
export interface PackageRuleInputConfig extends Record<string, unknown> {
versioning?: string;
packageFile?: string;
lockFiles?: string[];
depType?: string;
depTypes?: string[];
depName?: string;

View file

@ -302,8 +302,7 @@ export async function validateConfig(
}
const selectors = [
'matchFiles',
'matchPaths',
'matchFileNames',
'matchLanguages',
'matchBaseBranches',
'matchManagers',

View file

@ -64,7 +64,7 @@ export async function extractPackageFile(
const error = new Error(CONFIG_VALIDATION);
error.validationSource = packageFile;
error.validationError =
'Nested package.json must not contain renovate configuration. Please use `packageRules` with `matchPaths` in your main config instead.';
'Nested package.json must not contain Renovate configuration. Please use `packageRules` with `matchFileNames` in your main config instead.';
throw error;
}
const packageJsonName = packageJson.name;

View file

@ -1,7 +1,7 @@
import { FilesMatcher } from './files';
import { FileNamesMatcher } from './files';
describe('util/package-rules/files', () => {
const fileMatcher = new FilesMatcher();
const fileMatcher = new FileNamesMatcher();
describe('match', () => {
it('should return false if packageFile is not defined', () => {
@ -10,7 +10,7 @@ describe('util/package-rules/files', () => {
packageFile: undefined,
},
{
matchFiles: ['frontend/package.json'],
matchFileNames: ['frontend/package.json'],
}
);
expect(result).toBeFalse();

View file

@ -1,23 +1,27 @@
import is from '@sindresorhus/is';
import { minimatch } from 'minimatch';
import type { PackageRule, PackageRuleInputConfig } from '../../config/types';
import { Matcher } from './base';
export class FilesMatcher extends Matcher {
export class FileNamesMatcher extends Matcher {
override matches(
{ packageFile, lockFiles }: PackageRuleInputConfig,
{ matchFiles }: PackageRule
{ matchFileNames }: PackageRule
): boolean | null {
if (is.undefined(matchFiles)) {
if (is.undefined(matchFileNames)) {
return null;
}
if (is.undefined(packageFile)) {
return false;
}
return matchFiles.some(
(fileName) =>
packageFile === fileName ||
(is.array(lockFiles) && lockFiles?.includes(fileName))
return matchFileNames.some(
(matchFileName) =>
minimatch(packageFile, matchFileName, { dot: true }) ||
(is.array(lockFiles) &&
lockFiles.some((lockFile) =>
minimatch(lockFile, matchFileName, { dot: true })
))
);
}
}

View file

@ -930,7 +930,7 @@ describe('util/package-rules/index', () => {
packageFile: 'examples/foo/package.json',
packageRules: [
{
matchFiles: ['package.json'],
matchFileNames: ['package.json'],
x: 1,
},
],
@ -954,7 +954,7 @@ describe('util/package-rules/index', () => {
lockFiles: ['yarn.lock'],
packageRules: [
{
matchFiles: ['yarn.lock'],
matchFileNames: ['yarn.lock'],
x: 1,
},
],
@ -968,7 +968,7 @@ describe('util/package-rules/index', () => {
packageFile: 'examples/foo/package.json',
packageRules: [
{
matchPaths: ['examples/**', 'lib/'],
matchFileNames: ['examples/**', 'lib/'],
x: 1,
},
],

View file

@ -5,14 +5,13 @@ import { DatasourcesMatcher } from './datasources';
import { DepNameMatcher } from './dep-names';
import { DepPatternsMatcher } from './dep-patterns';
import { DepTypesMatcher } from './dep-types';
import { FilesMatcher } from './files';
import { FileNamesMatcher } from './files';
import { LanguagesMatcher } from './languages';
import { ManagersMatcher } from './managers';
import { MergeConfidenceMatcher } from './merge-confidence';
import { PackageNameMatcher } from './package-names';
import { PackagePatternsMatcher } from './package-patterns';
import { PackagePrefixesMatcher } from './package-prefixes';
import { PathsMatcher } from './paths';
import { SourceUrlPrefixesMatcher } from './sourceurl-prefixes';
import { SourceUrlsMatcher } from './sourceurls';
import type { MatcherApi } from './types';
@ -29,8 +28,7 @@ matchers.push([
new PackagePatternsMatcher(),
new PackagePrefixesMatcher(),
]);
matchers.push([new FilesMatcher()]);
matchers.push([new PathsMatcher()]);
matchers.push([new FileNamesMatcher()]);
matchers.push([new DepTypesMatcher()]);
matchers.push([new LanguagesMatcher()]);
matchers.push([new BaseBranchesMatcher()]);

View file

@ -1,45 +0,0 @@
import { logger } from '../../logger';
import { PathsMatcher } from './paths';
describe('util/package-rules/paths', () => {
const pathsMatcher = new PathsMatcher();
describe('match', () => {
it('should return false if packageFile is not defined', () => {
const result = pathsMatcher.matches(
{
packageFile: undefined,
},
{
matchPaths: ['opentelemetry/http'],
}
);
expect(result).toBeFalse();
});
it('should return false on partial match only', () => {
const result = pathsMatcher.matches(
{
packageFile: 'opentelemetry/http/package.json',
},
{
matchPaths: ['opentelemetry/http'],
}
);
expect(result).toBeFalse();
});
it('should return true and not log warning on partial and glob match', () => {
const result = pathsMatcher.matches(
{
packageFile: 'package.json',
},
{
matchPaths: ['package.json'],
}
);
expect(result).toBeTrue();
expect(logger.warn).not.toHaveBeenCalled();
});
});
});

View file

@ -1,22 +0,0 @@
import is from '@sindresorhus/is';
import { minimatch } from 'minimatch';
import type { PackageRule, PackageRuleInputConfig } from '../../config/types';
import { Matcher } from './base';
export class PathsMatcher extends Matcher {
override matches(
{ packageFile }: PackageRuleInputConfig,
{ matchPaths }: PackageRule
): boolean | null {
if (is.undefined(matchPaths)) {
return null;
}
if (is.undefined(packageFile)) {
return false;
}
return matchPaths.some((rulePath) =>
minimatch(packageFile, rulePath, { dot: true })
);
}
}

View file

@ -7,7 +7,7 @@ exports[`workers/repository/init/vulnerability detectVulnerabilityAlerts() retur
"matchDatasources": [
"npm",
],
"matchFiles": [
"matchFileNames": [
"backend/package-lock.json",
],
"matchPackageNames": [
@ -31,7 +31,7 @@ exports[`workers/repository/init/vulnerability detectVulnerabilityAlerts() retur
"matchDatasources": [
"go",
],
"matchFiles": [
"matchFileNames": [
"go.mod",
],
"matchPackageNames": [
@ -61,7 +61,7 @@ go",
"matchDatasources": [
"github-tags",
],
"matchFiles": [
"matchFileNames": [
".github/workflows/build.yaml",
],
"matchPackageNames": [
@ -91,7 +91,7 @@ actions",
"matchDatasources": [
"pypi",
],
"matchFiles": [
"matchFileNames": [
"requirements.txt",
],
"matchPackageNames": [
@ -136,7 +136,7 @@ Ansible before versions 2.1.4, 2.2.1 is vulnerable to an improper input validati
"matchDatasources": [
"maven",
],
"matchFiles": [
"matchFileNames": [
"pom.xml",
],
"matchPackageNames": [

View file

@ -328,7 +328,7 @@ describe('workers/repository/init/vulnerability', () => {
const res = await detectVulnerabilityAlerts(config);
expect(res.packageRules).toMatchSnapshot();
expect(res.packageRules).toHaveLength(5);
expect(res.packageRules?.[1]?.matchFiles?.[0]).toBe('go.mod');
expect(res.packageRules?.[1]?.matchFileNames?.[0]).toBe('go.mod');
expect(res.packageRules?.[2]?.matchCurrentVersion).toBe('1.8.2');
expect(res.remediations).toMatchSnapshot({
'backend/package-lock.json': [

View file

@ -208,7 +208,7 @@ export async function detectVulnerabilityAlerts(
datasource === PypiDatasource.id
? `==${val.firstPatchedVersion!}`
: val.firstPatchedVersion;
const matchFiles =
const matchFileNames =
datasource === GoDatasource.id
? [fileName.replace('go.sum', 'go.mod')]
: [fileName];
@ -216,7 +216,7 @@ export async function detectVulnerabilityAlerts(
matchDatasources: [datasource],
matchPackageNames: [depName],
matchCurrentVersion,
matchFiles,
matchFileNames,
};
const supportedRemediationFileTypes = ['package-lock.json'];
if (
@ -252,7 +252,7 @@ export async function detectVulnerabilityAlerts(
// istanbul ignore if
if (
config.transitiveRemediation &&
matchRule.matchFiles?.[0] === 'package.json'
matchRule.matchFileNames?.[0] === 'package.json'
) {
matchRule.force!.rangeStrategy = 'replace';
}

View file

@ -25,7 +25,7 @@ describe('workers/repository/updates/flatten', () => {
automerge: true,
},
{
matchPaths: ['frontend/package.json'],
matchFileNames: ['frontend/package.json'],
lockFileMaintenance: {
enabled: false,
},