mirror of
https://github.com/renovatebot/renovate.git
synced 2025-01-26 14:36:26 +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';
|
import * as configValidation from './validation';
|
||||||
|
|
||||||
describe('config/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)', () => {
|
describe('validateConfig(config)', () => {
|
||||||
it('returns deprecation warnings', async () => {
|
it('returns deprecation warnings', async () => {
|
||||||
const config = {
|
const config = {
|
||||||
|
|
|
@ -1,11 +1,6 @@
|
||||||
import is from '@sindresorhus/is';
|
import is from '@sindresorhus/is';
|
||||||
import { logger } from '../logger';
|
|
||||||
import { allManagersList, getManagerList } from '../modules/manager';
|
import { allManagersList, getManagerList } from '../modules/manager';
|
||||||
import { isCustomManager } from '../modules/manager/custom';
|
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 { CustomManager } from '../modules/manager/custom/types';
|
||||||
import type { HostRule } from '../types';
|
import type { HostRule } from '../types';
|
||||||
import { getExpression } from '../util/jsonata';
|
import { getExpression } from '../util/jsonata';
|
||||||
|
@ -39,6 +34,13 @@ import { allowedStatusCheckStrings } from './types';
|
||||||
import * as managerValidator from './validation-helpers/managers';
|
import * as managerValidator from './validation-helpers/managers';
|
||||||
import * as matchBaseBranchesValidator from './validation-helpers/match-base-branches';
|
import * as matchBaseBranchesValidator from './validation-helpers/match-base-branches';
|
||||||
import * as regexOrGlobValidator from './validation-helpers/regex-glob-matchers';
|
import * as regexOrGlobValidator from './validation-helpers/regex-glob-matchers';
|
||||||
|
import {
|
||||||
|
getParentName,
|
||||||
|
isFalseGlobal,
|
||||||
|
validateNumber,
|
||||||
|
validatePlainObject,
|
||||||
|
validateRegexManagerFields,
|
||||||
|
} from './validation-helpers/utils';
|
||||||
|
|
||||||
const options = getOptions();
|
const options = getOptions();
|
||||||
|
|
||||||
|
@ -84,42 +86,6 @@ function isIgnored(key: string): boolean {
|
||||||
return ignoredNodes.includes(key);
|
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[] {
|
function getUnsupportedEnabledManagers(enabledManagers: string[]): string[] {
|
||||||
return enabledManagers.filter(
|
return enabledManagers.filter(
|
||||||
(manager) => !allManagersList.includes(manager.replace('custom.', '')),
|
(manager) => !allManagersList.includes(manager.replace('custom.', '')),
|
||||||
|
@ -186,16 +152,6 @@ function initOptions(): void {
|
||||||
optionsInitialized = true;
|
optionsInitialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getParentName(parentPath: string | undefined): string {
|
|
||||||
return parentPath
|
|
||||||
? parentPath
|
|
||||||
.replace(regEx(/\.?encrypted$/), '')
|
|
||||||
.replace(regEx(/\[\d+\]$/), '')
|
|
||||||
.split('.')
|
|
||||||
.pop()!
|
|
||||||
: '.';
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function validateConfig(
|
export async function validateConfig(
|
||||||
configType: 'global' | 'inherit' | 'repo',
|
configType: 'global' | 'inherit' | 'repo',
|
||||||
config: RenovateConfig,
|
config: RenovateConfig,
|
||||||
|
@ -370,7 +326,8 @@ export async function validateConfig(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (type === 'integer') {
|
} 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) {
|
} else if (type === 'array' && val) {
|
||||||
if (is.array(val)) {
|
if (is.array(val)) {
|
||||||
for (const [subIndex, subval] of val.entries()) {
|
for (const [subIndex, subval] of val.entries()) {
|
||||||
|
@ -865,65 +822,6 @@ export async function validateConfig(
|
||||||
return { errors, warnings };
|
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
|
* Basic validation for global config options
|
||||||
*/
|
*/
|
||||||
|
@ -1013,7 +911,8 @@ async function validateGlobalConfig(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (type === 'integer') {
|
} 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') {
|
} else if (type === 'boolean') {
|
||||||
if (val !== true && val !== false) {
|
if (val !== true && val !== false) {
|
||||||
warnings.push({
|
warnings.push({
|
||||||
|
@ -1079,8 +978,15 @@ async function validateGlobalConfig(
|
||||||
}
|
}
|
||||||
} else if (key === 'cacheTtlOverride') {
|
} else if (key === 'cacheTtlOverride') {
|
||||||
for (const [subKey, subValue] of Object.entries(val)) {
|
for (const [subKey, subValue] of Object.entries(val)) {
|
||||||
|
const allowsNegative = optionAllowsNegativeIntegers.has(key);
|
||||||
warnings.push(
|
warnings.push(
|
||||||
...validateNumber(key, subValue, currentPath, subKey),
|
...validateNumber(
|
||||||
|
key,
|
||||||
|
subValue,
|
||||||
|
allowsNegative,
|
||||||
|
currentPath,
|
||||||
|
subKey,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} 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