aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--lib/config/validation-helpers/utils.spec.ts17
-rw-r--r--lib/config/validation-helpers/utils.ts138
-rw-r--r--lib/config/validation.spec.ts16
-rw-r--r--lib/config/validation.ts151
4 files changed, 174 insertions, 148 deletions
diff --git a/lib/config/validation-helpers/utils.spec.ts b/lib/config/validation-helpers/utils.spec.ts
new file mode 100644
index 00000000000..8344eed039e
--- /dev/null
+++ b/lib/config/validation-helpers/utils.spec.ts
@@ -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');
+ });
+ });
+});
diff --git a/lib/config/validation-helpers/utils.ts b/lib/config/validation-helpers/utils.ts
new file mode 100644
index 00000000000..5d676bed324
--- /dev/null
+++ b/lib/config/validation-helpers/utils.ts
@@ -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`,
+ });
+ }
+}
diff --git a/lib/config/validation.spec.ts b/lib/config/validation.spec.ts
index a49e75c0de3..927650a07d7 100644
--- a/lib/config/validation.spec.ts
+++ b/lib/config/validation.spec.ts
@@ -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 = {
diff --git a/lib/config/validation.ts b/lib/config/validation.ts
index d139cb42c10..551c07ba9f0 100644
--- a/lib/config/validation.ts
+++ b/lib/config/validation.ts
@@ -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;
-}