Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(config/validation): validate options which support regex/glob matching #28693

Merged
merged 14 commits into from
May 5, 2024
4 changes: 4 additions & 0 deletions lib/config/options/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const options: RenovateOptions[] = [
default: ['X-*'],
subType: 'string',
globalOnly: true,
patternMatch: true,
},
{
name: 'allowedEnv',
Expand All @@ -22,6 +23,7 @@ const options: RenovateOptions[] = [
default: [],
subType: 'string',
globalOnly: true,
patternMatch: true,
},
{
name: 'detectGlobalManagerConfig',
Expand Down Expand Up @@ -949,6 +951,7 @@ const options: RenovateOptions[] = [
default: null,
globalOnly: true,
supportedPlatforms: ['bitbucket'],
patternMatch: true,
},
{
name: 'autodiscoverTopics',
Expand Down Expand Up @@ -1230,6 +1233,7 @@ const options: RenovateOptions[] = [
mergeable: true,
cli: false,
env: false,
patternMatch: true,
},
{
name: 'excludeRepositories',
Expand Down
5 changes: 5 additions & 0 deletions lib/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,11 @@ export interface RenovateOptionBase {
* This is used to add depreciation message in the docs
*/
deprecationMsg?: string;

/**
* For internal use only: add it to any config option that supports regex or glob matching
*/
patternMatch?: boolean;
}

export interface RenovateArrayOption<
Expand Down
33 changes: 33 additions & 0 deletions lib/config/validation-helpers/regex-glob-matchers.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { check } from './regex-glob-matchers';

describe('config/validation-helpers/regex-glob-matchers', () => {
it('should error for multiple match alls', () => {
const res = check({
val: ['*', '**'],
currentPath: 'hostRules[0].allowedHeaders',
});
expect(res).toHaveLength(1);
});

it('should error for invalid regex', () => {
const res = check({
val: ['[', '/[/', '/.*[/'],
currentPath: 'hostRules[0].allowedHeaders',
});
expect(res).toHaveLength(2);
});

it('should error for non-strings', () => {
const res = check({
val: ['*', 2],
currentPath: 'hostRules[0].allowedHeaders',
});
expect(res).toMatchObject([
{
message:
'hostRules[0].allowedHeaders: should be an array of strings. You have included object.',
topic: 'Configuration Error',
},
]);
});
});
45 changes: 45 additions & 0 deletions lib/config/validation-helpers/regex-glob-matchers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import is from '@sindresorhus/is';
import { getRegexPredicate, isRegexMatch } from '../../util/string-match';
import type { ValidationMessage } from '../types';
import type { CheckMatcherArgs } from './types';

/**
* Only if type condition or context condition violated then errors array will be mutated to store metadata
*/
export function check({
val: matchers,
currentPath,
}: CheckMatcherArgs): ValidationMessage[] {
const res: ValidationMessage[] = [];

if (is.array(matchers, is.string)) {
if (
(matchers.includes('*') || matchers.includes('**')) &&
matchers.length > 1
) {
res.push({
topic: 'Configuration Error',
message: `${currentPath}: Your input contains * or ** along with other patterns. Please remove them, as * or ** matches all patterns.`,
});
}
for (const matcher of matchers) {
// Validate regex pattern
if (isRegexMatch(matcher)) {
const autodiscoveryPred = getRegexPredicate(matcher);
if (!autodiscoveryPred) {
rarkins marked this conversation as resolved.
Show resolved Hide resolved
res.push({
topic: 'Configuration Error',
message: `Failed to parse regex pattern "${matcher}"`,
});
}
}
}
} else {
res.push({
topic: 'Configuration Error',
message: `${currentPath}: should be an array of strings. You have included ${typeof matchers}.`,
});
}

return res;
}
5 changes: 5 additions & 0 deletions lib/config/validation-helpers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,8 @@ export interface CheckManagerArgs {
resolvedRule: PackageRule;
currentPath: string;
}

export interface CheckMatcherArgs {
val: unknown;
currentPath: string;
}
68 changes: 68 additions & 0 deletions lib/config/validation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1225,6 +1225,34 @@ describe('config/validation', () => {
expect(warnings).toHaveLength(0);
expect(errors).toHaveLength(2);
});

it('catches when * or ** is combined with others patterns in a regexOrGlob option', async () => {
const config = {
packageRules: [
{
matchRepositories: ['groupA/**', 'groupB/**'], // valid
enabled: false,
},
{
matchRepositories: ['*', 'repo'], // invalid
enabled: true,
},
],
};
const { errors, warnings } = await configValidation.validateConfig(
'repo',
config,
);
expect(errors).toMatchObject([
{
message:
'packageRules[1].matchRepositories: Your input contains * or ** along with other patterns. Please remove them, as * or ** matches all patterns.',
topic: 'Configuration Error',
},
]);
expect(errors).toHaveLength(1);
RahulGautamSingh marked this conversation as resolved.
Show resolved Hide resolved
expect(warnings).toHaveLength(0);
});
});

describe('validateConfig() -> globaOnly options', () => {
Expand Down Expand Up @@ -1706,5 +1734,45 @@ describe('config/validation', () => {
expect(warnings).toHaveLength(0);
expect(errors).toHaveLength(0);
});

it('catches when * or ** is combined with others patterns in a regexOrGlob option', async () => {
const config = {
packageRules: [
{
matchRepositories: ['*', 'repo'], // invalid
enabled: false,
},
],
allowedHeaders: ['*', '**'], // invalid
autodiscoverProjects: ['**', 'project'], // invalid
allowedEnv: ['env_var'], // valid
};
const { errors, warnings } = await configValidation.validateConfig(
'global',
config,
);
expect(warnings).toMatchObject([
{
message:
'allowedHeaders: Your input contains * or ** along with other patterns. Please remove them, as * or ** matches all patterns.',
topic: 'Configuration Error',
},
{
message:
'autodiscoverProjects: Your input contains * or ** along with other patterns. Please remove them, as * or ** matches all patterns.',
topic: 'Configuration Error',
},
]);

expect(errors).toMatchObject([
{
message:
'packageRules[0].matchRepositories: Your input contains * or ** along with other patterns. Please remove them, as * or ** matches all patterns.',
topic: 'Configuration Error',
},
]);
expect(warnings).toHaveLength(2);
expect(errors).toHaveLength(1);
});
});
});
87 changes: 58 additions & 29 deletions lib/config/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,16 @@ import {
allowedStatusCheckStrings,
} from './types';
import * as managerValidator from './validation-helpers/managers';
import * as regexOrGlobValidator from './validation-helpers/regex-glob-matchers';

const options = getOptions();

let optionsInitialized = false;
let optionTypes: Record<string, RenovateOptions['type']>;
let optionParents: Record<string, AllowedParents[]>;
let optionGlobals: Set<string>;
let optionInherits: Set<string>;
let optionRegexOrGlob: Set<string>;

const managerList = getManagerList();

Expand Down Expand Up @@ -100,27 +103,49 @@ function getDeprecationMessage(option: string): string | undefined {
}

function isInhertConfigOption(key: string): boolean {
if (!optionInherits) {
optionInherits = new Set();
for (const option of options) {
if (option.inheritConfigSupport) {
optionInherits.add(option.name);
}
}
}
return optionInherits.has(key);
}

function isRegexOrGlobOption(key: string): boolean {
return optionRegexOrGlob.has(key);
}

function isGlobalOption(key: string): boolean {
if (!optionGlobals) {
optionGlobals = new Set();
for (const option of options) {
if (option.globalOnly) {
optionGlobals.add(option.name);
}
return optionGlobals.has(key);
}

function initOptions(): void {
if (optionsInitialized) {
return;
}

optionParents = {};
RahulGautamSingh marked this conversation as resolved.
Show resolved Hide resolved
optionInherits = new Set();
optionTypes = {};
optionRegexOrGlob = new Set();
optionGlobals = new Set();

for (const option of options) {
optionTypes[option.name] = option.type;

if (option.parents) {
optionParents[option.name] = option.parents;
}

if (option.inheritConfigSupport) {
optionInherits.add(option.name);
}

if (option.patternMatch) {
optionRegexOrGlob.add(option.name);
}

if (option.globalOnly) {
optionGlobals.add(option.name);
}
}
return optionGlobals.has(key);

optionsInitialized = true;
}

export function getParentName(parentPath: string | undefined): string {
Expand All @@ -139,20 +164,8 @@ export async function validateConfig(
isPreset?: boolean,
parentPath?: string,
): Promise<ValidationResult> {
if (!optionTypes) {
optionTypes = {};
options.forEach((option) => {
optionTypes[option.name] = option.type;
});
}
if (!optionParents) {
optionParents = {};
options.forEach((option) => {
if (option.parents) {
optionParents[option.name] = option.parents;
}
});
}
initOptions();

let errors: ValidationMessage[] = [];
let warnings: ValidationMessage[] = [];

Expand Down Expand Up @@ -354,6 +367,14 @@ export async function validateConfig(
errors = errors.concat(subValidation.errors);
}
}
if (isRegexOrGlobOption(key)) {
errors.push(
...regexOrGlobValidator.check({
val,
currentPath,
}),
);
}
if (key === 'extends') {
for (const subval of val) {
if (is.string(subval)) {
Expand Down Expand Up @@ -951,6 +972,14 @@ async function validateGlobalConfig(
}
} else if (type === 'array') {
if (is.array(val)) {
if (isRegexOrGlobOption(key)) {
warnings.push(
...regexOrGlobValidator.check({
val,
currentPath: currentPath!,
}),
);
}
if (key === 'gitNoVerify') {
const allowedValues = ['commit', 'push'];
for (const value of val as string[]) {
Expand Down
1 change: 1 addition & 0 deletions tools/docs/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ function genTable(obj: [string, string][], type: string, def: any): string {
'experimentalIssues',
'advancedUse',
'deprecationMsg',
'patternMatch',
rarkins marked this conversation as resolved.
Show resolved Hide resolved
];
obj.forEach(([key, val]) => {
const el = [key, val];
Expand Down