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
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
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
25 changes: 25 additions & 0 deletions lib/config/validation-helpers/regex-glob-matchers.spec.ts
@@ -0,0 +1,25 @@
import { check } from './regex-glob-matchers';

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

it('should have errors - 2', () => {
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',
},
]);
});
});
30 changes: 30 additions & 0 deletions lib/config/validation-helpers/regex-glob-matchers.ts
@@ -0,0 +1,30 @@
import is from '@sindresorhus/is';
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,
RahulGautamSingh marked this conversation as resolved.
Show resolved Hide resolved
currentPath,
}: CheckMatcherArgs): ValidationMessage[] {
let errMessage: string | undefined;

if (is.array(val, is.string)) {
if ((val.includes('*') || val.includes('**')) && val.length > 1) {
errMessage = `${currentPath}: Your input contains * or ** along with other patterns. Please remove them, as * or ** matches all patterns.`;
}
} else {
errMessage = `${currentPath}: should be an array of strings. You have included ${typeof val}.`;
}

return errMessage
? [
{
topic: 'Configuration Error',
message: errMessage,
},
]
: [];
}
5 changes: 5 additions & 0 deletions lib/config/validation-helpers/types.ts
Expand Up @@ -4,3 +4,8 @@ export interface CheckManagerArgs {
resolvedRule: PackageRule;
currentPath: string;
}

export interface CheckMatcherArgs {
val: unknown;
currentPath: string;
}
64 changes: 64 additions & 0 deletions lib/config/validation.spec.ts
Expand Up @@ -1225,6 +1225,30 @@
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/**'],
enabled: false,
},
],
};
const { errors, warnings } = await configValidation.validateConfig(
'repo',
config,
);
expect(errors).toMatchObject([

Check failure on line 1242 in lib/config/validation.spec.ts

View workflow job for this annotation

GitHub Actions / test (16/16)

config/validation › validateConfig(config) › catches when * or ** is combined with others patterns in a regexOrGlob option

expect(received).toMatchObject(expected) - Expected - 6 + Received + 1 - Array [ - Object { - "message": "packageRules[0].matchRepositories: Your input contains * or ** along with other patterns. Please remove them, as * or ** matches all patterns.", - "topic": "Configuration Error", - }, - ] + Array [] at Object.<anonymous> (lib/config/validation.spec.ts:1242:22)
{
message:
'packageRules[0].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 +1730,45 @@
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);
});
});
});
83 changes: 55 additions & 28 deletions lib/config/validation.ts
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,45 @@ 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 {
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 +160,10 @@ 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;
}
});
if (!optionsInitialized) {
initOptions();
}

RahulGautamSingh marked this conversation as resolved.
Show resolved Hide resolved
let errors: ValidationMessage[] = [];
let warnings: ValidationMessage[] = [];

Expand Down Expand Up @@ -354,6 +365,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 +970,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
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