2019-07-17 08:14:56 +00:00
|
|
|
import is from '@sindresorhus/is';
|
2023-08-15 17:12:54 +00:00
|
|
|
import { allManagersList, getManagerList } from '../modules/manager';
|
2023-09-12 14:16:01 +00:00
|
|
|
import { isCustomManager } from '../modules/manager/custom';
|
2023-09-13 05:31:48 +00:00
|
|
|
import type {
|
|
|
|
RegexManagerConfig,
|
|
|
|
RegexManagerTemplates,
|
|
|
|
} from '../modules/manager/custom/regex/types';
|
|
|
|
import type { CustomManager } from '../modules/manager/custom/types';
|
2020-11-10 09:12:03 +00:00
|
|
|
import { configRegexPredicate, isConfigRegex, regEx } from '../util/regex';
|
2020-04-05 08:09:55 +00:00
|
|
|
import * as template from '../util/template';
|
2022-03-03 09:53:23 +00:00
|
|
|
import {
|
|
|
|
hasValidSchedule,
|
|
|
|
hasValidTimezone,
|
|
|
|
} from '../workers/repository/update/branch/schedule';
|
2021-05-04 06:02:39 +00:00
|
|
|
import { migrateConfig } from './migration';
|
2021-08-15 05:25:30 +00:00
|
|
|
import { getOptions } from './options';
|
2022-08-06 05:27:07 +00:00
|
|
|
import { resolveConfigPresets } from './presets';
|
2021-03-02 20:44:55 +00:00
|
|
|
import type {
|
|
|
|
RenovateConfig,
|
|
|
|
RenovateOptions,
|
|
|
|
ValidationMessage,
|
2021-05-11 10:51:21 +00:00
|
|
|
ValidationResult,
|
2021-03-02 20:44:55 +00:00
|
|
|
} from './types';
|
2020-05-01 16:03:48 +00:00
|
|
|
import * as managerValidator from './validation-helpers/managers';
|
2019-07-17 08:14:56 +00:00
|
|
|
|
2019-08-23 13:46:31 +00:00
|
|
|
const options = getOptions();
|
2017-07-28 19:15:27 +00:00
|
|
|
|
2019-08-23 13:46:31 +00:00
|
|
|
let optionTypes: Record<string, RenovateOptions['type']>;
|
2021-04-05 10:41:31 +00:00
|
|
|
let optionParents: Record<string, RenovateOptions['parent']>;
|
2017-07-28 19:15:27 +00:00
|
|
|
|
2020-10-18 05:56:16 +00:00
|
|
|
const managerList = getManagerList();
|
|
|
|
|
2023-07-04 09:41:19 +00:00
|
|
|
const topLevelObjects = managerList;
|
2021-05-20 10:25:22 +00:00
|
|
|
|
|
|
|
const ignoredNodes = [
|
|
|
|
'$schema',
|
|
|
|
'depType',
|
|
|
|
'npmToken',
|
|
|
|
'packageFile',
|
|
|
|
'forkToken',
|
|
|
|
'repository',
|
|
|
|
'vulnerabilityAlertsOnly',
|
|
|
|
'vulnerabilityAlert',
|
|
|
|
'isVulnerabilityAlert',
|
|
|
|
'copyLocalLibs', // deprecated - functionality is now enabled by default
|
|
|
|
'prBody', // deprecated
|
2021-06-23 20:19:14 +00:00
|
|
|
'minimumConfidence', // undocumented feature flag
|
2021-05-20 10:25:22 +00:00
|
|
|
];
|
2021-12-29 06:26:13 +00:00
|
|
|
const tzRe = regEx(/^:timezone\((.+)\)$/);
|
|
|
|
const rulesRe = regEx(/p.*Rules\[\d+\]$/);
|
2022-12-26 18:30:44 +00:00
|
|
|
|
2020-10-18 05:56:16 +00:00
|
|
|
function isManagerPath(parentPath: string): boolean {
|
|
|
|
return (
|
2023-09-24 08:55:56 +00:00
|
|
|
regEx(/^customManagers\[[0-9]+]$/).test(parentPath) ||
|
2020-10-18 05:56:16 +00:00
|
|
|
managerList.includes(parentPath)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-05-20 10:25:22 +00:00
|
|
|
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 getUnsupportedEnabledManagers(enabledManagers: string[]): string[] {
|
|
|
|
return enabledManagers.filter(
|
2023-11-07 15:50:29 +00:00
|
|
|
(manager) => !allManagersList.includes(manager),
|
2021-05-20 10:25:22 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-04-24 22:48:54 +00:00
|
|
|
function getDeprecationMessage(option: string): string | undefined {
|
|
|
|
const deprecatedOptions: Record<string, string | undefined> = {
|
2021-05-20 10:25:22 +00:00
|
|
|
branchName: `Direct editing of branchName is now deprecated. Please edit branchPrefix, additionalBranchPrefix, or branchTopic instead`,
|
|
|
|
commitMessage: `Direct editing of commitMessage is now deprecated. Please edit commitMessage's subcomponents instead.`,
|
|
|
|
prTitle: `Direct editing of prTitle is now deprecated. Please edit commitMessage subcomponents instead as they will be passed through to prTitle.`,
|
|
|
|
};
|
|
|
|
return deprecatedOptions[option];
|
|
|
|
}
|
|
|
|
|
2022-04-24 22:48:54 +00:00
|
|
|
export function getParentName(parentPath: string | undefined): string {
|
2021-04-05 10:41:31 +00:00
|
|
|
return parentPath
|
|
|
|
? parentPath
|
2021-10-19 12:53:34 +00:00
|
|
|
.replace(regEx(/\.?encrypted$/), '')
|
|
|
|
.replace(regEx(/\[\d+\]$/), '')
|
2021-04-05 10:41:31 +00:00
|
|
|
.split('.')
|
2022-04-24 22:48:54 +00:00
|
|
|
.pop()!
|
2021-04-05 10:41:31 +00:00
|
|
|
: '.';
|
|
|
|
}
|
|
|
|
|
2019-08-23 13:46:31 +00:00
|
|
|
export async function validateConfig(
|
|
|
|
config: RenovateConfig,
|
|
|
|
isPreset?: boolean,
|
2023-11-07 15:50:29 +00:00
|
|
|
parentPath?: string,
|
2019-08-23 13:46:31 +00:00
|
|
|
): Promise<ValidationResult> {
|
2017-11-03 06:43:26 +00:00
|
|
|
if (!optionTypes) {
|
|
|
|
optionTypes = {};
|
2020-04-12 16:09:36 +00:00
|
|
|
options.forEach((option) => {
|
2017-11-03 06:43:26 +00:00
|
|
|
optionTypes[option.name] = option.type;
|
|
|
|
});
|
|
|
|
}
|
2021-04-05 10:41:31 +00:00
|
|
|
if (!optionParents) {
|
|
|
|
optionParents = {};
|
|
|
|
options.forEach((option) => {
|
|
|
|
if (option.parent) {
|
|
|
|
optionParents[option.name] = option.parent;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2019-08-23 13:46:31 +00:00
|
|
|
let errors: ValidationMessage[] = [];
|
|
|
|
let warnings: ValidationMessage[] = [];
|
2017-07-28 19:15:27 +00:00
|
|
|
|
2017-11-10 12:46:16 +00:00
|
|
|
for (const [key, val] of Object.entries(config)) {
|
2018-04-11 19:38:31 +00:00
|
|
|
const currentPath = parentPath ? `${parentPath}.${key}` : key;
|
2020-10-16 08:23:50 +00:00
|
|
|
// istanbul ignore if
|
|
|
|
if (key === '__proto__') {
|
|
|
|
errors.push({
|
2021-04-01 13:50:17 +00:00
|
|
|
topic: 'Config security error',
|
2020-10-16 08:23:50 +00:00
|
|
|
message: '__proto__',
|
|
|
|
});
|
2021-11-08 19:20:03 +00:00
|
|
|
continue;
|
2020-10-16 08:23:50 +00:00
|
|
|
}
|
2021-04-12 04:11:25 +00:00
|
|
|
if (parentPath && topLevelObjects.includes(key)) {
|
|
|
|
errors.push({
|
|
|
|
topic: 'Configuration Error',
|
|
|
|
message: `The "${key}" object can only be configured at the top level of a config but was found inside "${parentPath}"`,
|
|
|
|
});
|
|
|
|
}
|
2021-04-15 09:18:51 +00:00
|
|
|
if (key === 'enabledManagers' && val) {
|
|
|
|
const unsupportedManagers = getUnsupportedEnabledManagers(
|
2023-11-07 15:50:29 +00:00
|
|
|
val as string[],
|
2021-04-15 09:18:51 +00:00
|
|
|
);
|
|
|
|
if (is.nonEmptyArray(unsupportedManagers)) {
|
|
|
|
errors.push({
|
|
|
|
topic: 'Configuration Error',
|
|
|
|
message: `The following managers configured in enabledManagers are not supported: "${unsupportedManagers.join(
|
2023-11-07 15:50:29 +00:00
|
|
|
', ',
|
2021-04-15 09:18:51 +00:00
|
|
|
)}"`,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
2020-10-18 05:56:16 +00:00
|
|
|
if (key === 'fileMatch') {
|
|
|
|
if (parentPath === undefined) {
|
|
|
|
errors.push({
|
2021-04-01 13:50:17 +00:00
|
|
|
topic: 'Config error',
|
2020-10-18 05:56:16 +00:00
|
|
|
message: `"fileMatch" may not be defined at the top level of a config and must instead be within a manager block`,
|
|
|
|
});
|
|
|
|
} else if (!isManagerPath(parentPath)) {
|
|
|
|
warnings.push({
|
2021-04-01 13:50:17 +00:00
|
|
|
topic: 'Config warning',
|
2020-10-18 05:56:16 +00:00
|
|
|
message: `"fileMatch" must be configured in a manager block and not here: ${parentPath}`,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
2017-08-02 06:54:42 +00:00
|
|
|
if (
|
2017-07-28 19:15:27 +00:00
|
|
|
!isIgnored(key) && // We need to ignore some reserved keys
|
2019-08-28 04:46:48 +00:00
|
|
|
!(is as any).function(val) // Ignore all functions
|
2017-07-28 19:15:27 +00:00
|
|
|
) {
|
2018-04-17 06:39:26 +00:00
|
|
|
if (getDeprecationMessage(key)) {
|
|
|
|
warnings.push({
|
2021-04-01 13:50:17 +00:00
|
|
|
topic: 'Deprecation Warning',
|
2022-04-24 22:48:54 +00:00
|
|
|
message: getDeprecationMessage(key)!,
|
2018-04-17 06:39:26 +00:00
|
|
|
});
|
|
|
|
}
|
2020-01-27 11:48:08 +00:00
|
|
|
const templateKeys = [
|
|
|
|
'branchName',
|
|
|
|
'commitBody',
|
|
|
|
'commitMessage',
|
|
|
|
'prTitle',
|
|
|
|
'semanticCommitScope',
|
|
|
|
];
|
2020-03-06 08:07:55 +00:00
|
|
|
if ((key.endsWith('Template') || templateKeys.includes(key)) && val) {
|
2020-01-27 11:48:08 +00:00
|
|
|
try {
|
2023-08-15 09:31:15 +00:00
|
|
|
// TODO: validate string #22198
|
2022-04-24 22:48:54 +00:00
|
|
|
let res = template.compile((val as string).toString(), config, false);
|
2021-02-03 15:33:28 +00:00
|
|
|
res = template.compile(res, config, false);
|
|
|
|
template.compile(res, config, false);
|
2020-01-27 11:48:08 +00:00
|
|
|
} catch (err) {
|
|
|
|
errors.push({
|
2021-04-01 13:50:17 +00:00
|
|
|
topic: 'Configuration Error',
|
2020-04-05 08:09:55 +00:00
|
|
|
message: `Invalid template in config path: ${currentPath}`,
|
2020-01-27 11:48:08 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
2021-04-05 10:41:31 +00:00
|
|
|
const parentName = getParentName(parentPath);
|
|
|
|
if (
|
|
|
|
!isPreset &&
|
|
|
|
optionParents[key] &&
|
|
|
|
optionParents[key] !== parentName
|
|
|
|
) {
|
2023-08-15 09:31:15 +00:00
|
|
|
// TODO: types (#22198)
|
2021-04-05 10:41:31 +00:00
|
|
|
const message = `${key} should only be configured within a "${optionParents[key]}" object. Was found in ${parentName}`;
|
|
|
|
warnings.push({
|
|
|
|
topic: `${parentPath ? `${parentPath}.` : ''}${key}`,
|
|
|
|
message,
|
|
|
|
});
|
|
|
|
}
|
2017-07-28 19:15:27 +00:00
|
|
|
if (!optionTypes[key]) {
|
|
|
|
errors.push({
|
2021-04-01 13:50:17 +00:00
|
|
|
topic: 'Configuration Error',
|
2019-03-17 06:21:25 +00:00
|
|
|
message: `Invalid configuration option: ${currentPath}`,
|
2017-07-28 19:15:27 +00:00
|
|
|
});
|
2017-08-14 09:09:14 +00:00
|
|
|
} else if (key === 'schedule') {
|
2020-03-02 11:06:16 +00:00
|
|
|
const [validSchedule, errorMessage] = hasValidSchedule(val as string[]);
|
2017-08-14 09:09:14 +00:00
|
|
|
if (!validSchedule) {
|
|
|
|
errors.push({
|
2021-04-01 13:50:17 +00:00
|
|
|
topic: 'Configuration Error',
|
2018-04-11 19:38:31 +00:00
|
|
|
message: `Invalid ${currentPath}: \`${errorMessage}\``,
|
2017-08-14 09:09:14 +00:00
|
|
|
});
|
|
|
|
}
|
2020-11-12 07:21:05 +00:00
|
|
|
} else if (
|
|
|
|
['allowedVersions', 'matchCurrentVersion'].includes(key) &&
|
|
|
|
isConfigRegex(val)
|
|
|
|
) {
|
2020-11-10 09:12:03 +00:00
|
|
|
if (!configRegexPredicate(val)) {
|
2020-04-15 20:07:53 +00:00
|
|
|
errors.push({
|
2021-04-01 13:50:17 +00:00
|
|
|
topic: 'Configuration Error',
|
2020-04-15 20:07:53 +00:00
|
|
|
message: `Invalid regExp for ${currentPath}: \`${val}\``,
|
|
|
|
});
|
|
|
|
}
|
2022-09-25 06:56:02 +00:00
|
|
|
} else if (
|
|
|
|
key === 'matchCurrentValue' &&
|
|
|
|
is.string(val) &&
|
|
|
|
!configRegexPredicate(val)
|
|
|
|
) {
|
|
|
|
errors.push({
|
|
|
|
topic: 'Configuration Error',
|
|
|
|
message: `Invalid regExp for ${currentPath}: \`${val}\``,
|
|
|
|
});
|
2018-03-12 03:24:45 +00:00
|
|
|
} else if (key === 'timezone' && val !== null) {
|
2020-03-02 11:06:16 +00:00
|
|
|
const [validTimezone, errorMessage] = hasValidTimezone(val as string);
|
2018-03-12 03:24:45 +00:00
|
|
|
if (!validTimezone) {
|
|
|
|
errors.push({
|
2021-04-01 13:50:17 +00:00
|
|
|
topic: 'Configuration Error',
|
2018-04-11 19:38:31 +00:00
|
|
|
message: `${currentPath}: ${errorMessage}`,
|
2018-03-12 03:24:45 +00:00
|
|
|
});
|
|
|
|
}
|
2021-11-09 07:02:59 +00:00
|
|
|
} else if (val !== null) {
|
2017-08-14 05:49:33 +00:00
|
|
|
const type = optionTypes[key];
|
2017-07-28 19:15:27 +00:00
|
|
|
if (type === 'boolean') {
|
|
|
|
if (val !== true && val !== false) {
|
|
|
|
errors.push({
|
2021-04-01 13:50:17 +00:00
|
|
|
topic: 'Configuration Error',
|
2018-04-11 19:38:31 +00:00
|
|
|
message: `Configuration option \`${currentPath}\` should be boolean. Found: ${JSON.stringify(
|
2023-11-07 15:50:29 +00:00
|
|
|
val,
|
2017-08-22 06:12:42 +00:00
|
|
|
)} (${typeof val})`,
|
2017-07-28 19:15:27 +00:00
|
|
|
});
|
|
|
|
}
|
2022-04-20 20:44:19 +00:00
|
|
|
} else if (type === 'integer') {
|
|
|
|
if (!is.number(val)) {
|
|
|
|
errors.push({
|
|
|
|
topic: 'Configuration Error',
|
|
|
|
message: `Configuration option \`${currentPath}\` should be an integer. Found: ${JSON.stringify(
|
2023-11-07 15:50:29 +00:00
|
|
|
val,
|
2022-04-20 20:44:19 +00:00
|
|
|
)} (${typeof val})`,
|
|
|
|
});
|
|
|
|
}
|
2019-03-31 06:01:06 +00:00
|
|
|
} else if (type === 'array' && val) {
|
2021-03-04 05:21:55 +00:00
|
|
|
if (is.array(val)) {
|
2018-04-11 19:38:31 +00:00
|
|
|
for (const [subIndex, subval] of val.entries()) {
|
2018-06-04 18:07:22 +00:00
|
|
|
if (is.object(subval)) {
|
2021-12-22 13:20:58 +00:00
|
|
|
const subValidation = await validateConfig(
|
|
|
|
subval as RenovateConfig,
|
2018-04-12 10:13:39 +00:00
|
|
|
isPreset,
|
2023-11-07 15:50:29 +00:00
|
|
|
`${currentPath}[${subIndex}]`,
|
2018-03-28 08:04:07 +00:00
|
|
|
);
|
2018-03-06 14:54:27 +00:00
|
|
|
warnings = warnings.concat(subValidation.warnings);
|
|
|
|
errors = errors.concat(subValidation.errors);
|
2017-08-02 05:52:28 +00:00
|
|
|
}
|
2018-03-28 07:37:19 +00:00
|
|
|
}
|
|
|
|
if (key === 'extends') {
|
|
|
|
for (const subval of val) {
|
2020-10-27 15:39:11 +00:00
|
|
|
if (is.string(subval)) {
|
2021-05-05 15:46:34 +00:00
|
|
|
if (
|
|
|
|
parentName === 'packageRules' &&
|
|
|
|
subval.startsWith('group:')
|
|
|
|
) {
|
2021-05-04 06:02:39 +00:00
|
|
|
warnings.push({
|
|
|
|
topic: 'Configuration Warning',
|
|
|
|
message: `${currentPath}: you should not extend "group:" presets`,
|
|
|
|
});
|
|
|
|
}
|
2020-10-27 15:39:11 +00:00
|
|
|
if (tzRe.test(subval)) {
|
2022-04-24 22:48:54 +00:00
|
|
|
const [, timezone] = tzRe.exec(subval)!;
|
2021-05-17 08:06:24 +00:00
|
|
|
const [validTimezone, errorMessage] =
|
|
|
|
hasValidTimezone(timezone);
|
2020-10-27 15:39:11 +00:00
|
|
|
if (!validTimezone) {
|
|
|
|
errors.push({
|
2021-04-01 13:50:17 +00:00
|
|
|
topic: 'Configuration Error',
|
2020-10-27 15:39:11 +00:00
|
|
|
message: `${currentPath}: ${errorMessage}`,
|
|
|
|
});
|
|
|
|
}
|
2018-03-28 07:37:19 +00:00
|
|
|
}
|
2020-10-27 15:39:11 +00:00
|
|
|
} else {
|
|
|
|
errors.push({
|
2021-04-01 13:50:17 +00:00
|
|
|
topic: 'Configuration Warning',
|
2020-10-27 15:39:11 +00:00
|
|
|
message: `${currentPath}: preset value is not a string`,
|
|
|
|
});
|
2018-03-28 07:37:19 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-04-12 10:13:39 +00:00
|
|
|
|
|
|
|
const selectors = [
|
2023-05-25 15:46:28 +00:00
|
|
|
'matchFileNames',
|
2021-01-29 10:43:42 +00:00
|
|
|
'matchLanguages',
|
2023-07-04 09:41:19 +00:00
|
|
|
'matchCategories',
|
2021-01-29 10:43:42 +00:00
|
|
|
'matchBaseBranches',
|
|
|
|
'matchManagers',
|
|
|
|
'matchDatasources',
|
|
|
|
'matchDepTypes',
|
2022-12-26 18:30:44 +00:00
|
|
|
'matchDepNames',
|
|
|
|
'matchDepPatterns',
|
2021-01-29 10:43:42 +00:00
|
|
|
'matchPackageNames',
|
|
|
|
'matchPackagePatterns',
|
2021-04-03 05:18:25 +00:00
|
|
|
'matchPackagePrefixes',
|
2022-12-26 18:30:44 +00:00
|
|
|
'excludeDepNames',
|
|
|
|
'excludeDepPatterns',
|
2018-04-12 10:13:39 +00:00
|
|
|
'excludePackageNames',
|
|
|
|
'excludePackagePatterns',
|
2021-04-03 05:18:25 +00:00
|
|
|
'excludePackagePrefixes',
|
2023-07-21 04:21:36 +00:00
|
|
|
'excludeRepositories',
|
2022-09-25 06:56:02 +00:00
|
|
|
'matchCurrentValue',
|
2020-07-22 06:32:01 +00:00
|
|
|
'matchCurrentVersion',
|
2021-01-29 10:43:42 +00:00
|
|
|
'matchSourceUrlPrefixes',
|
2022-03-28 07:58:20 +00:00
|
|
|
'matchSourceUrls',
|
2021-01-29 10:43:42 +00:00
|
|
|
'matchUpdateTypes',
|
2023-03-21 18:37:38 +00:00
|
|
|
'matchConfidence',
|
2023-07-21 04:21:36 +00:00
|
|
|
'matchRepositories',
|
2018-04-12 10:13:39 +00:00
|
|
|
];
|
2018-03-28 08:04:07 +00:00
|
|
|
if (key === 'packageRules') {
|
2021-04-11 16:38:25 +00:00
|
|
|
for (const [subIndex, packageRule] of val.entries()) {
|
2018-06-04 18:07:22 +00:00
|
|
|
if (is.object(packageRule)) {
|
2021-05-04 06:02:39 +00:00
|
|
|
const resolvedRule = migrateConfig({
|
|
|
|
packageRules: [
|
|
|
|
await resolveConfigPresets(
|
|
|
|
packageRule as RenovateConfig,
|
2023-11-07 15:50:29 +00:00
|
|
|
config,
|
2021-05-04 06:02:39 +00:00
|
|
|
),
|
|
|
|
],
|
2022-04-24 22:48:54 +00:00
|
|
|
}).migratedConfig.packageRules![0];
|
2019-02-20 21:29:38 +00:00
|
|
|
errors.push(
|
2023-11-07 15:50:29 +00:00
|
|
|
...managerValidator.check({ resolvedRule, currentPath }),
|
2019-02-20 21:29:38 +00:00
|
|
|
);
|
2021-05-17 08:06:24 +00:00
|
|
|
const selectorLength = Object.keys(resolvedRule).filter(
|
2023-11-07 15:50:29 +00:00
|
|
|
(ruleKey) => selectors.includes(ruleKey),
|
2021-05-17 08:06:24 +00:00
|
|
|
).length;
|
2021-04-11 16:40:01 +00:00
|
|
|
if (!selectorLength) {
|
2021-04-11 16:38:25 +00:00
|
|
|
const message = `${currentPath}[${subIndex}]: Each packageRule must contain at least one match* or exclude* selector. Rule: ${JSON.stringify(
|
2023-11-07 15:50:29 +00:00
|
|
|
packageRule,
|
2021-04-11 16:38:25 +00:00
|
|
|
)}`;
|
2018-03-28 13:13:32 +00:00
|
|
|
errors.push({
|
2021-04-01 13:50:17 +00:00
|
|
|
topic: 'Configuration Error',
|
2018-03-28 08:04:07 +00:00
|
|
|
message,
|
|
|
|
});
|
|
|
|
}
|
2021-04-11 17:26:20 +00:00
|
|
|
if (selectorLength === Object.keys(resolvedRule).length) {
|
|
|
|
const message = `${currentPath}[${subIndex}]: Each packageRule must contain at least one non-match* or non-exclude* field. Rule: ${JSON.stringify(
|
2023-11-07 15:50:29 +00:00
|
|
|
packageRule,
|
2021-04-11 17:26:20 +00:00
|
|
|
)}`;
|
|
|
|
warnings.push({
|
|
|
|
topic: 'Configuration Error',
|
|
|
|
message,
|
|
|
|
});
|
|
|
|
}
|
2021-04-22 07:16:40 +00:00
|
|
|
// It's too late to apply any of these options once you already have updates determined
|
|
|
|
const preLookupOptions = [
|
2021-04-22 07:55:14 +00:00
|
|
|
'allowedVersions',
|
2021-04-22 07:16:40 +00:00
|
|
|
'extractVersion',
|
|
|
|
'followTag',
|
|
|
|
'ignoreDeps',
|
|
|
|
'ignoreUnstable',
|
|
|
|
'rangeStrategy',
|
|
|
|
'registryUrls',
|
|
|
|
'respectLatest',
|
|
|
|
'rollbackPrs',
|
|
|
|
'separateMajorMinor',
|
|
|
|
'separateMinorPatch',
|
|
|
|
'separateMultipleMajor',
|
|
|
|
'versioning',
|
|
|
|
];
|
|
|
|
if (is.nonEmptyArray(resolvedRule.matchUpdateTypes)) {
|
|
|
|
for (const option of preLookupOptions) {
|
|
|
|
if (resolvedRule[option] !== undefined) {
|
|
|
|
const message = `${currentPath}[${subIndex}]: packageRules cannot combine both matchUpdateTypes and ${option}. Rule: ${JSON.stringify(
|
2023-11-07 15:50:29 +00:00
|
|
|
packageRule,
|
2021-04-22 07:16:40 +00:00
|
|
|
)}`;
|
|
|
|
errors.push({
|
|
|
|
topic: 'Configuration Error',
|
|
|
|
message,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-03-28 08:04:07 +00:00
|
|
|
} else {
|
|
|
|
errors.push({
|
2021-04-01 13:50:17 +00:00
|
|
|
topic: 'Configuration Error',
|
2018-04-11 19:38:31 +00:00
|
|
|
message: `${currentPath} must contain JSON objects`,
|
2018-03-28 08:04:07 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-09-24 08:55:56 +00:00
|
|
|
if (key === 'customManagers') {
|
2020-03-06 08:07:55 +00:00
|
|
|
const allowedKeys = [
|
2023-08-22 14:01:03 +00:00
|
|
|
'customType',
|
2021-02-01 06:03:38 +00:00
|
|
|
'description',
|
2020-03-06 08:07:55 +00:00
|
|
|
'fileMatch',
|
|
|
|
'matchStrings',
|
2020-11-27 05:55:57 +00:00
|
|
|
'matchStringsStrategy',
|
2020-03-06 08:07:55 +00:00
|
|
|
'depNameTemplate',
|
2022-03-03 15:08:43 +00:00
|
|
|
'packageNameTemplate',
|
2020-03-06 08:07:55 +00:00
|
|
|
'datasourceTemplate',
|
|
|
|
'versioningTemplate',
|
2021-03-22 06:18:34 +00:00
|
|
|
'registryUrlTemplate',
|
2021-06-11 07:49:09 +00:00
|
|
|
'currentValueTemplate',
|
2021-06-15 11:10:23 +00:00
|
|
|
'extractVersionTemplate',
|
2021-10-05 12:21:11 +00:00
|
|
|
'autoReplaceStringTemplate',
|
2021-10-25 05:25:35 +00:00
|
|
|
'depTypeTemplate',
|
2020-03-06 08:07:55 +00:00
|
|
|
];
|
2023-09-24 08:55:56 +00:00
|
|
|
for (const customManager of val as CustomManager[]) {
|
2020-03-06 08:07:55 +00:00
|
|
|
if (
|
2023-09-24 08:55:56 +00:00
|
|
|
Object.keys(customManager).some(
|
2023-11-07 15:50:29 +00:00
|
|
|
(k) => !allowedKeys.includes(k),
|
2020-04-12 16:09:36 +00:00
|
|
|
)
|
2020-03-06 08:07:55 +00:00
|
|
|
) {
|
2023-09-24 08:55:56 +00:00
|
|
|
const disallowedKeys = Object.keys(customManager).filter(
|
2023-11-07 15:50:29 +00:00
|
|
|
(k) => !allowedKeys.includes(k),
|
2020-03-06 08:07:55 +00:00
|
|
|
);
|
|
|
|
errors.push({
|
2021-04-01 13:50:17 +00:00
|
|
|
topic: 'Configuration Error',
|
2023-09-24 08:55:56 +00:00
|
|
|
message: `Custom Manager contains disallowed fields: ${disallowedKeys.join(
|
2023-11-07 15:50:29 +00:00
|
|
|
', ',
|
2020-03-06 08:07:55 +00:00
|
|
|
)}`,
|
|
|
|
});
|
2023-09-12 14:16:01 +00:00
|
|
|
} else if (
|
2023-09-24 08:55:56 +00:00
|
|
|
is.nonEmptyString(customManager.customType) &&
|
|
|
|
isCustomManager(customManager.customType)
|
2023-09-12 14:16:01 +00:00
|
|
|
) {
|
2023-09-24 08:55:56 +00:00
|
|
|
if (is.nonEmptyArray(customManager.fileMatch)) {
|
|
|
|
switch (customManager.customType) {
|
2023-09-12 14:16:01 +00:00
|
|
|
case 'regex':
|
|
|
|
validateRegexManagerFields(
|
2023-09-24 08:55:56 +00:00
|
|
|
customManager,
|
2023-09-12 14:16:01 +00:00
|
|
|
currentPath,
|
2023-11-07 15:50:29 +00:00
|
|
|
errors,
|
2023-09-12 14:16:01 +00:00
|
|
|
);
|
|
|
|
break;
|
2021-11-22 20:08:10 +00:00
|
|
|
}
|
2021-11-23 14:17:49 +00:00
|
|
|
} else {
|
|
|
|
errors.push({
|
|
|
|
topic: 'Configuration Error',
|
2023-09-24 08:55:56 +00:00
|
|
|
message: `Each Custom Manager must contain a non-empty fileMatch array`,
|
2021-11-23 14:17:49 +00:00
|
|
|
});
|
2020-03-06 08:07:55 +00:00
|
|
|
}
|
2021-03-04 05:21:55 +00:00
|
|
|
} else {
|
2023-09-12 14:16:01 +00:00
|
|
|
if (
|
2023-09-24 08:55:56 +00:00
|
|
|
is.emptyString(customManager.customType) ||
|
|
|
|
is.undefined(customManager.customType)
|
2023-09-12 14:16:01 +00:00
|
|
|
) {
|
|
|
|
errors.push({
|
|
|
|
topic: 'Configuration Error',
|
2023-09-24 08:55:56 +00:00
|
|
|
message: `Each Custom Manager must contain a non-empty customType string`,
|
2023-09-12 14:16:01 +00:00
|
|
|
});
|
|
|
|
} else {
|
|
|
|
errors.push({
|
|
|
|
topic: 'Configuration Error',
|
2023-09-24 08:55:56 +00:00
|
|
|
message: `Invalid customType: ${customManager.customType}. Key is not a custom manager`,
|
2023-09-12 14:16:01 +00:00
|
|
|
});
|
|
|
|
}
|
2020-03-06 08:07:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-01-29 10:43:42 +00:00
|
|
|
if (
|
2022-12-26 18:30:44 +00:00
|
|
|
[
|
|
|
|
'matchPackagePatterns',
|
|
|
|
'excludePackagePatterns',
|
|
|
|
'matchDepPatterns',
|
|
|
|
'excludeDepPatterns',
|
|
|
|
].includes(key)
|
2021-01-29 10:43:42 +00:00
|
|
|
) {
|
2020-07-30 04:54:20 +00:00
|
|
|
for (const pattern of val as string[]) {
|
2019-10-22 06:48:40 +00:00
|
|
|
if (pattern !== '*') {
|
|
|
|
try {
|
|
|
|
regEx(pattern);
|
|
|
|
} catch (e) {
|
2019-10-15 08:14:49 +00:00
|
|
|
errors.push({
|
2021-04-01 13:50:17 +00:00
|
|
|
topic: 'Configuration Error',
|
2019-10-22 06:48:40 +00:00
|
|
|
message: `Invalid regExp for ${currentPath}: \`${pattern}\``,
|
2019-10-15 08:14:49 +00:00
|
|
|
});
|
|
|
|
}
|
2018-04-30 11:18:51 +00:00
|
|
|
}
|
2019-10-22 06:48:40 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if (key === 'fileMatch') {
|
2020-07-30 04:54:20 +00:00
|
|
|
for (const fileMatch of val as string[]) {
|
2019-10-22 06:48:40 +00:00
|
|
|
try {
|
|
|
|
regEx(fileMatch);
|
|
|
|
} catch (e) {
|
|
|
|
errors.push({
|
2021-04-01 13:50:17 +00:00
|
|
|
topic: 'Configuration Error',
|
2019-10-22 06:48:40 +00:00
|
|
|
message: `Invalid regExp for ${currentPath}: \`${fileMatch}\``,
|
|
|
|
});
|
|
|
|
}
|
2018-04-30 11:18:51 +00:00
|
|
|
}
|
|
|
|
}
|
2023-02-20 23:32:08 +00:00
|
|
|
if (key === 'baseBranches') {
|
|
|
|
for (const baseBranch of val as string[]) {
|
|
|
|
if (
|
|
|
|
isConfigRegex(baseBranch) &&
|
|
|
|
!configRegexPredicate(baseBranch)
|
|
|
|
) {
|
|
|
|
errors.push({
|
|
|
|
topic: 'Configuration Error',
|
|
|
|
message: `Invalid regExp for ${currentPath}: \`${baseBranch}\``,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-04-12 10:13:39 +00:00
|
|
|
if (
|
2022-09-25 06:56:02 +00:00
|
|
|
(selectors.includes(key) ||
|
|
|
|
key === 'matchCurrentVersion' ||
|
|
|
|
key === 'matchCurrentValue') &&
|
2023-08-15 09:31:15 +00:00
|
|
|
// TODO: can be undefined ? #22198
|
2022-04-24 22:48:54 +00:00
|
|
|
!rulesRe.test(parentPath!) && // Inside a packageRule
|
2023-08-02 15:07:49 +00:00
|
|
|
(is.string(parentPath) || !isPreset) // top level in a preset
|
2018-04-12 10:13:39 +00:00
|
|
|
) {
|
|
|
|
errors.push({
|
2021-04-01 13:50:17 +00:00
|
|
|
topic: 'Configuration Error',
|
2018-04-12 10:13:39 +00:00
|
|
|
message: `${currentPath}: ${key} should be inside a \`packageRule\` only`,
|
|
|
|
});
|
|
|
|
}
|
2021-03-04 05:21:55 +00:00
|
|
|
} else {
|
|
|
|
errors.push({
|
2021-04-01 13:50:17 +00:00
|
|
|
topic: 'Configuration Error',
|
2021-03-04 05:21:55 +00:00
|
|
|
message: `Configuration option \`${currentPath}\` should be a list (Array)`,
|
|
|
|
});
|
2017-07-28 19:15:27 +00:00
|
|
|
}
|
|
|
|
} else if (type === 'string') {
|
2018-06-04 18:07:22 +00:00
|
|
|
if (!is.string(val)) {
|
2017-07-28 19:15:27 +00:00
|
|
|
errors.push({
|
2021-04-01 13:50:17 +00:00
|
|
|
topic: 'Configuration Error',
|
2018-04-11 19:38:31 +00:00
|
|
|
message: `Configuration option \`${currentPath}\` should be a string`,
|
2017-07-28 19:15:27 +00:00
|
|
|
});
|
|
|
|
}
|
2020-09-30 09:02:25 +00:00
|
|
|
} else if (
|
|
|
|
type === 'object' &&
|
|
|
|
currentPath !== 'compatibility' &&
|
2020-10-27 07:13:23 +00:00
|
|
|
currentPath !== 'constraints' &&
|
|
|
|
currentPath !== 'force.constraints'
|
2020-09-30 09:02:25 +00:00
|
|
|
) {
|
2020-08-26 12:59:50 +00:00
|
|
|
if (is.plainObject(val)) {
|
2022-06-10 05:14:49 +00:00
|
|
|
if (key === 'registryAliases') {
|
2023-06-16 08:38:47 +00:00
|
|
|
const res = validatePlainObject(val);
|
2021-05-20 10:25:22 +00:00
|
|
|
if (res !== true) {
|
|
|
|
errors.push({
|
|
|
|
topic: 'Configuration Error',
|
2023-06-16 08:38:47 +00:00
|
|
|
message: `Invalid \`${currentPath}.${key}.${res}\` configuration: value is not a string`,
|
2021-05-20 10:25:22 +00:00
|
|
|
});
|
|
|
|
}
|
2023-07-10 18:22:35 +00:00
|
|
|
} else if (key === 'customDatasources') {
|
|
|
|
const allowedKeys = [
|
|
|
|
'description',
|
|
|
|
'defaultRegistryUrlTemplate',
|
|
|
|
'format',
|
|
|
|
'transformTemplates',
|
|
|
|
];
|
|
|
|
for (const [
|
|
|
|
customDatasourceName,
|
|
|
|
customDatasourceValue,
|
|
|
|
] of Object.entries(val)) {
|
|
|
|
if (!is.plainObject(customDatasourceValue)) {
|
|
|
|
errors.push({
|
|
|
|
topic: 'Configuration Error',
|
|
|
|
message: `Invalid \`${currentPath}.${customDatasourceName}\` configuration: customDatasource is not an object`,
|
|
|
|
});
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
for (const [subKey, subValue] of Object.entries(
|
2023-11-07 15:50:29 +00:00
|
|
|
customDatasourceValue,
|
2023-07-10 18:22:35 +00:00
|
|
|
)) {
|
|
|
|
if (!allowedKeys.includes(subKey)) {
|
|
|
|
errors.push({
|
|
|
|
topic: 'Configuration Error',
|
|
|
|
message: `Invalid \`${currentPath}.${key}.${subKey}\` configuration: key is not allowed`,
|
|
|
|
});
|
|
|
|
} else if (subKey === 'transformTemplates') {
|
|
|
|
if (!is.array(subValue, is.string)) {
|
|
|
|
errors.push({
|
|
|
|
topic: 'Configuration Error',
|
|
|
|
message: `Invalid \`${currentPath}.${key}.${subKey}\` configuration: is not an array of string`,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
} else if (!is.string(subValue)) {
|
|
|
|
errors.push({
|
|
|
|
topic: 'Configuration Error',
|
|
|
|
message: `Invalid \`${currentPath}.${key}.${subKey}\` configuration: is a string`,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-05-28 08:04:26 +00:00
|
|
|
} else if (
|
2023-07-21 06:32:22 +00:00
|
|
|
[
|
|
|
|
'customEnvVariables',
|
|
|
|
'migratePresets',
|
|
|
|
'productLinks',
|
|
|
|
'secrets',
|
2023-08-08 15:36:36 +00:00
|
|
|
'customizeDashboard',
|
2023-07-21 06:32:22 +00:00
|
|
|
].includes(key)
|
2021-05-28 08:04:26 +00:00
|
|
|
) {
|
2021-05-20 10:25:22 +00:00
|
|
|
const res = validatePlainObject(val);
|
|
|
|
if (res !== true) {
|
2020-06-04 13:47:56 +00:00
|
|
|
errors.push({
|
2021-04-01 13:50:17 +00:00
|
|
|
topic: 'Configuration Error',
|
2021-05-20 10:25:22 +00:00
|
|
|
message: `Invalid \`${currentPath}.${key}.${res}\` configuration: value is not a string`,
|
2020-06-04 13:47:56 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
const ignoredObjects = options
|
|
|
|
.filter((option) => option.freeChoice)
|
|
|
|
.map((option) => option.name);
|
|
|
|
if (!ignoredObjects.includes(key)) {
|
2021-12-22 13:20:58 +00:00
|
|
|
const subValidation = await validateConfig(
|
2020-06-04 13:47:56 +00:00
|
|
|
val,
|
|
|
|
isPreset,
|
2023-11-07 15:50:29 +00:00
|
|
|
currentPath,
|
2020-06-04 13:47:56 +00:00
|
|
|
);
|
|
|
|
warnings = warnings.concat(subValidation.warnings);
|
|
|
|
errors = errors.concat(subValidation.errors);
|
|
|
|
}
|
2019-03-31 07:16:29 +00:00
|
|
|
}
|
2017-07-28 19:15:27 +00:00
|
|
|
} else {
|
|
|
|
errors.push({
|
2021-04-01 13:50:17 +00:00
|
|
|
topic: 'Configuration Error',
|
2018-04-11 19:38:31 +00:00
|
|
|
message: `Configuration option \`${currentPath}\` should be a json object`,
|
2017-07-28 19:15:27 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-12-26 18:30:44 +00:00
|
|
|
|
2019-08-23 13:46:31 +00:00
|
|
|
function sortAll(a: ValidationMessage, b: ValidationMessage): number {
|
2020-03-07 10:27:10 +00:00
|
|
|
// istanbul ignore else: currently never happen
|
2021-04-01 13:50:17 +00:00
|
|
|
if (a.topic === b.topic) {
|
2019-04-16 14:03:37 +00:00
|
|
|
return a.message > b.message ? 1 : -1;
|
2018-04-28 06:48:12 +00:00
|
|
|
}
|
2020-03-07 10:27:10 +00:00
|
|
|
// istanbul ignore next: currently never happen
|
2021-04-01 13:50:17 +00:00
|
|
|
return a.topic > b.topic ? 1 : -1;
|
2018-04-28 06:48:12 +00:00
|
|
|
}
|
2022-12-26 18:30:44 +00:00
|
|
|
|
2018-04-28 06:48:12 +00:00
|
|
|
errors.sort(sortAll);
|
|
|
|
warnings.sort(sortAll);
|
2017-07-31 12:50:44 +00:00
|
|
|
return { errors, warnings };
|
2017-07-28 19:15:27 +00:00
|
|
|
}
|
2023-09-12 14:16:01 +00:00
|
|
|
|
|
|
|
function validateRegexManagerFields(
|
2023-09-24 08:55:56 +00:00
|
|
|
customManager: Partial<RegexManagerConfig>,
|
2023-09-12 14:16:01 +00:00
|
|
|
currentPath: string,
|
2023-11-07 15:50:29 +00:00
|
|
|
errors: ValidationMessage[],
|
2023-09-12 14:16:01 +00:00
|
|
|
): void {
|
2023-09-24 08:55:56 +00:00
|
|
|
if (is.nonEmptyArray(customManager.matchStrings)) {
|
|
|
|
for (const matchString of customManager.matchStrings) {
|
2023-09-12 14:16:01 +00:00
|
|
|
try {
|
|
|
|
regEx(matchString);
|
|
|
|
} catch (e) {
|
|
|
|
errors.push({
|
|
|
|
topic: 'Configuration Error',
|
|
|
|
message: `Invalid regExp for ${currentPath}: \`${matchString}\``,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
errors.push({
|
|
|
|
topic: 'Configuration Error',
|
2023-09-24 08:55:56 +00:00
|
|
|
message: `Each Custom Manager must contain a non-empty matchStrings array`,
|
2023-09-12 14:16:01 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
const mandatoryFields = ['depName', 'currentValue', 'datasource'];
|
|
|
|
for (const field of mandatoryFields) {
|
|
|
|
const templateField = `${field}Template` as keyof RegexManagerTemplates;
|
|
|
|
if (
|
2023-09-24 08:55:56 +00:00
|
|
|
!customManager[templateField] &&
|
|
|
|
!customManager.matchStrings?.some((matchString) =>
|
2023-11-07 15:50:29 +00:00
|
|
|
matchString.includes(`(?<${field}>`),
|
2023-09-12 14:16:01 +00:00
|
|
|
)
|
|
|
|
) {
|
|
|
|
errors.push({
|
|
|
|
topic: 'Configuration Error',
|
|
|
|
message: `Regex Managers must contain ${field}Template configuration or regex group named ${field}`,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|