mirror of
https://github.com/renovatebot/renovate.git
synced 2025-01-25 14:06:27 +00:00
refactor(config/validation): move helper fns to separate file (#33206)
This commit is contained in:
parent
94ccb91d31
commit
f98db7404b
4 changed files with 174 additions and 148 deletions
17
lib/config/validation-helpers/utils.spec.ts
Normal file
17
lib/config/validation-helpers/utils.spec.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { getParentName } from './utils';
|
||||
|
||||
describe('config/validation-helpers/utils', () => {
|
||||
describe('getParentName()', () => {
|
||||
it('ignores encrypted in root', () => {
|
||||
expect(getParentName('encrypted')).toBeEmptyString();
|
||||
});
|
||||
|
||||
it('handles array types', () => {
|
||||
expect(getParentName('hostRules[1]')).toBe('hostRules');
|
||||
});
|
||||
|
||||
it('handles encrypted within array types', () => {
|
||||
expect(getParentName('hostRules[0].encrypted')).toBe('hostRules');
|
||||
});
|
||||
});
|
||||
});
|
138
lib/config/validation-helpers/utils.ts
Normal file
138
lib/config/validation-helpers/utils.ts
Normal file
|
@ -0,0 +1,138 @@
|
|||
import is from '@sindresorhus/is';
|
||||
import { logger } from '../../logger';
|
||||
import type {
|
||||
RegexManagerConfig,
|
||||
RegexManagerTemplates,
|
||||
} from '../../modules/manager/custom/regex/types';
|
||||
import { regEx } from '../../util/regex';
|
||||
import type { ValidationMessage } from '../types';
|
||||
|
||||
export function getParentName(parentPath: string | undefined): string {
|
||||
return parentPath
|
||||
? parentPath
|
||||
.replace(regEx(/\.?encrypted$/), '')
|
||||
.replace(regEx(/\[\d+\]$/), '')
|
||||
.split('.')
|
||||
.pop()!
|
||||
: '.';
|
||||
}
|
||||
|
||||
export function validatePlainObject(
|
||||
val: Record<string, unknown>,
|
||||
): true | string {
|
||||
for (const [key, value] of Object.entries(val)) {
|
||||
if (!is.string(value)) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function validateNumber(
|
||||
key: string,
|
||||
val: unknown,
|
||||
allowsNegative: boolean,
|
||||
currentPath?: string,
|
||||
subKey?: string,
|
||||
): ValidationMessage[] {
|
||||
const errors: ValidationMessage[] = [];
|
||||
const path = `${currentPath}${subKey ? '.' + subKey : ''}`;
|
||||
if (is.number(val)) {
|
||||
if (val < 0 && !allowsNegative) {
|
||||
errors.push({
|
||||
topic: 'Configuration Error',
|
||||
message: `Configuration option \`${path}\` should be a positive integer. Found negative value instead.`,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
errors.push({
|
||||
topic: 'Configuration Error',
|
||||
message: `Configuration option \`${path}\` should be an integer. Found: ${JSON.stringify(
|
||||
val,
|
||||
)} (${typeof val}).`,
|
||||
});
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
/** An option is a false global if it has the same name as a global only option
|
||||
* but is actually just the field of a non global option or field an children of the non global option
|
||||
* eg. token: it's global option used as the bot's token as well and
|
||||
* also it can be the token used for a platform inside the hostRules configuration
|
||||
*/
|
||||
export function isFalseGlobal(
|
||||
optionName: string,
|
||||
parentPath?: string,
|
||||
): boolean {
|
||||
if (parentPath?.includes('hostRules')) {
|
||||
if (
|
||||
optionName === 'token' ||
|
||||
optionName === 'username' ||
|
||||
optionName === 'password'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasField(
|
||||
customManager: Partial<RegexManagerConfig>,
|
||||
field: string,
|
||||
): boolean {
|
||||
const templateField = `${field}Template` as keyof RegexManagerTemplates;
|
||||
return !!(
|
||||
customManager[templateField] ??
|
||||
customManager.matchStrings?.some((matchString) =>
|
||||
matchString.includes(`(?<${field}>`),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function validateRegexManagerFields(
|
||||
customManager: Partial<RegexManagerConfig>,
|
||||
currentPath: string,
|
||||
errors: ValidationMessage[],
|
||||
): void {
|
||||
if (is.nonEmptyArray(customManager.matchStrings)) {
|
||||
for (const matchString of customManager.matchStrings) {
|
||||
try {
|
||||
regEx(matchString);
|
||||
} catch (err) {
|
||||
logger.debug(
|
||||
{ err },
|
||||
'customManager.matchStrings regEx validation error',
|
||||
);
|
||||
errors.push({
|
||||
topic: 'Configuration Error',
|
||||
message: `Invalid regExp for ${currentPath}: \`${matchString}\``,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
errors.push({
|
||||
topic: 'Configuration Error',
|
||||
message: `Each Custom Manager must contain a non-empty matchStrings array`,
|
||||
});
|
||||
}
|
||||
|
||||
const mandatoryFields = ['currentValue', 'datasource'];
|
||||
for (const field of mandatoryFields) {
|
||||
if (!hasField(customManager, field)) {
|
||||
errors.push({
|
||||
topic: 'Configuration Error',
|
||||
message: `Regex Managers must contain ${field}Template configuration or regex group named ${field}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const nameFields = ['depName', 'packageName'];
|
||||
if (!nameFields.some((field) => hasField(customManager, field))) {
|
||||
errors.push({
|
||||
topic: 'Configuration Error',
|
||||
message: `Regex Managers must contain depName or packageName regex groups or templates`,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -4,22 +4,6 @@ import type { RenovateConfig } from './types';
|
|||
import * as configValidation from './validation';
|
||||
|
||||
describe('config/validation', () => {
|
||||
describe('getParentName()', () => {
|
||||
it('ignores encrypted in root', () => {
|
||||
expect(configValidation.getParentName('encrypted')).toBeEmptyString();
|
||||
});
|
||||
|
||||
it('handles array types', () => {
|
||||
expect(configValidation.getParentName('hostRules[1]')).toBe('hostRules');
|
||||
});
|
||||
|
||||
it('handles encrypted within array types', () => {
|
||||
expect(configValidation.getParentName('hostRules[0].encrypted')).toBe(
|
||||
'hostRules',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateConfig(config)', () => {
|
||||
it('returns deprecation warnings', async () => {
|
||||
const config = {
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
import is from '@sindresorhus/is';
|
||||
import { logger } from '../logger';
|
||||
import { allManagersList, getManagerList } from '../modules/manager';
|
||||
import { isCustomManager } from '../modules/manager/custom';
|
||||
import type {
|
||||
RegexManagerConfig,
|
||||
RegexManagerTemplates,
|
||||
} from '../modules/manager/custom/regex/types';
|
||||
import type { CustomManager } from '../modules/manager/custom/types';
|
||||
import type { HostRule } from '../types';
|
||||
import { getExpression } from '../util/jsonata';
|
||||
|
@ -39,6 +34,13 @@ import { allowedStatusCheckStrings } from './types';
|
|||
import * as managerValidator from './validation-helpers/managers';
|
||||
import * as matchBaseBranchesValidator from './validation-helpers/match-base-branches';
|
||||
import * as regexOrGlobValidator from './validation-helpers/regex-glob-matchers';
|
||||
import {
|
||||
getParentName,
|
||||
isFalseGlobal,
|
||||
validateNumber,
|
||||
validatePlainObject,
|
||||
validateRegexManagerFields,
|
||||
} from './validation-helpers/utils';
|
||||
|
||||
const options = getOptions();
|
||||
|
||||
|
@ -84,42 +86,6 @@ function isIgnored(key: string): boolean {
|
|||
return ignoredNodes.includes(key);
|
||||
}
|
||||
|
||||
function validatePlainObject(val: Record<string, unknown>): true | string {
|
||||
for (const [key, value] of Object.entries(val)) {
|
||||
if (!is.string(value)) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function validateNumber(
|
||||
key: string,
|
||||
val: unknown,
|
||||
currentPath?: string,
|
||||
subKey?: string,
|
||||
): ValidationMessage[] {
|
||||
const errors: ValidationMessage[] = [];
|
||||
const path = `${currentPath}${subKey ? '.' + subKey : ''}`;
|
||||
if (is.number(val)) {
|
||||
if (val < 0 && !optionAllowsNegativeIntegers.has(key)) {
|
||||
errors.push({
|
||||
topic: 'Configuration Error',
|
||||
message: `Configuration option \`${path}\` should be a positive integer. Found negative value instead.`,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
errors.push({
|
||||
topic: 'Configuration Error',
|
||||
message: `Configuration option \`${path}\` should be an integer. Found: ${JSON.stringify(
|
||||
val,
|
||||
)} (${typeof val}).`,
|
||||
});
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function getUnsupportedEnabledManagers(enabledManagers: string[]): string[] {
|
||||
return enabledManagers.filter(
|
||||
(manager) => !allManagersList.includes(manager.replace('custom.', '')),
|
||||
|
@ -186,16 +152,6 @@ function initOptions(): void {
|
|||
optionsInitialized = true;
|
||||
}
|
||||
|
||||
export function getParentName(parentPath: string | undefined): string {
|
||||
return parentPath
|
||||
? parentPath
|
||||
.replace(regEx(/\.?encrypted$/), '')
|
||||
.replace(regEx(/\[\d+\]$/), '')
|
||||
.split('.')
|
||||
.pop()!
|
||||
: '.';
|
||||
}
|
||||
|
||||
export async function validateConfig(
|
||||
configType: 'global' | 'inherit' | 'repo',
|
||||
config: RenovateConfig,
|
||||
|
@ -370,7 +326,8 @@ export async function validateConfig(
|
|||
});
|
||||
}
|
||||
} else if (type === 'integer') {
|
||||
errors.push(...validateNumber(key, val, currentPath));
|
||||
const allowsNegative = optionAllowsNegativeIntegers.has(key);
|
||||
errors.push(...validateNumber(key, val, allowsNegative, currentPath));
|
||||
} else if (type === 'array' && val) {
|
||||
if (is.array(val)) {
|
||||
for (const [subIndex, subval] of val.entries()) {
|
||||
|
@ -865,65 +822,6 @@ export async function validateConfig(
|
|||
return { errors, warnings };
|
||||
}
|
||||
|
||||
function hasField(
|
||||
customManager: Partial<RegexManagerConfig>,
|
||||
field: string,
|
||||
): boolean {
|
||||
const templateField = `${field}Template` as keyof RegexManagerTemplates;
|
||||
return !!(
|
||||
customManager[templateField] ??
|
||||
customManager.matchStrings?.some((matchString) =>
|
||||
matchString.includes(`(?<${field}>`),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function validateRegexManagerFields(
|
||||
customManager: Partial<RegexManagerConfig>,
|
||||
currentPath: string,
|
||||
errors: ValidationMessage[],
|
||||
): void {
|
||||
if (is.nonEmptyArray(customManager.matchStrings)) {
|
||||
for (const matchString of customManager.matchStrings) {
|
||||
try {
|
||||
regEx(matchString);
|
||||
} catch (err) {
|
||||
logger.debug(
|
||||
{ err },
|
||||
'customManager.matchStrings regEx validation error',
|
||||
);
|
||||
errors.push({
|
||||
topic: 'Configuration Error',
|
||||
message: `Invalid regExp for ${currentPath}: \`${matchString}\``,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
errors.push({
|
||||
topic: 'Configuration Error',
|
||||
message: `Each Custom Manager must contain a non-empty matchStrings array`,
|
||||
});
|
||||
}
|
||||
|
||||
const mandatoryFields = ['currentValue', 'datasource'];
|
||||
for (const field of mandatoryFields) {
|
||||
if (!hasField(customManager, field)) {
|
||||
errors.push({
|
||||
topic: 'Configuration Error',
|
||||
message: `Regex Managers must contain ${field}Template configuration or regex group named ${field}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const nameFields = ['depName', 'packageName'];
|
||||
if (!nameFields.some((field) => hasField(customManager, field))) {
|
||||
errors.push({
|
||||
topic: 'Configuration Error',
|
||||
message: `Regex Managers must contain depName or packageName regex groups or templates`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic validation for global config options
|
||||
*/
|
||||
|
@ -1013,7 +911,8 @@ async function validateGlobalConfig(
|
|||
});
|
||||
}
|
||||
} else if (type === 'integer') {
|
||||
warnings.push(...validateNumber(key, val, currentPath));
|
||||
const allowsNegative = optionAllowsNegativeIntegers.has(key);
|
||||
warnings.push(...validateNumber(key, val, allowsNegative, currentPath));
|
||||
} else if (type === 'boolean') {
|
||||
if (val !== true && val !== false) {
|
||||
warnings.push({
|
||||
|
@ -1079,8 +978,15 @@ async function validateGlobalConfig(
|
|||
}
|
||||
} else if (key === 'cacheTtlOverride') {
|
||||
for (const [subKey, subValue] of Object.entries(val)) {
|
||||
const allowsNegative = optionAllowsNegativeIntegers.has(key);
|
||||
warnings.push(
|
||||
...validateNumber(key, subValue, currentPath, subKey),
|
||||
...validateNumber(
|
||||
key,
|
||||
subValue,
|
||||
allowsNegative,
|
||||
currentPath,
|
||||
subKey,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
@ -1101,22 +1007,3 @@ async function validateGlobalConfig(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** An option is a false global if it has the same name as a global only option
|
||||
* but is actually just the field of a non global option or field an children of the non global option
|
||||
* eg. token: it's global option used as the bot's token as well and
|
||||
* also it can be the token used for a platform inside the hostRules configuration
|
||||
*/
|
||||
function isFalseGlobal(optionName: string, parentPath?: string): boolean {
|
||||
if (parentPath?.includes('hostRules')) {
|
||||
if (
|
||||
optionName === 'token' ||
|
||||
optionName === 'username' ||
|
||||
optionName === 'password'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue