Skip to content

Commit

Permalink
feat(config/validation): validate options which support regex/glob ma…
Browse files Browse the repository at this point in the history
…tching (#28693)

Co-authored-by: Sebastian Poxhofer <secustor@users.noreply.github.com>
Co-authored-by: Rhys Arkins <rhys@arkins.net>
  • Loading branch information
3 people authored May 5, 2024
1 parent 2910185 commit 265e628
Show file tree
Hide file tree
Showing 8 changed files with 218 additions and 29 deletions.
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 @@ -957,6 +959,7 @@ const options: RenovateOptions[] = [
default: null,
globalOnly: true,
supportedPlatforms: ['bitbucket'],
patternMatch: true,
},
{
name: 'autodiscoverTopics',
Expand Down Expand Up @@ -1238,6 +1241,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 @@ -443,6 +443,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',
},
]);
});
});
44 changes: 44 additions & 0 deletions lib/config/validation-helpers/regex-glob-matchers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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)) {
if (!getRegexPredicate(matcher)) {
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);
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 = {};
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 @@ -958,6 +979,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',
];
obj.forEach(([key, val]) => {
const el = [key, val];
Expand Down

0 comments on commit 265e628

Please sign in to comment.