2019-07-17 08:14:56 +00:00
|
|
|
import is from '@sindresorhus/is';
|
2020-01-27 11:48:08 +00:00
|
|
|
import * as handlebars from 'handlebars';
|
2019-08-23 13:46:31 +00:00
|
|
|
import { getOptions, RenovateOptions } from './definitions';
|
|
|
|
import { resolveConfigPresets } from './presets';
|
|
|
|
import { hasValidSchedule, hasValidTimezone } from '../workers/branch/schedule';
|
|
|
|
import * as managerValidator from './validation-helpers/managers';
|
|
|
|
import { RenovateConfig, ValidationMessage } from './common';
|
2019-10-22 06:48:40 +00:00
|
|
|
import { regEx } from '../util/regex';
|
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']>;
|
2017-07-28 19:15:27 +00:00
|
|
|
|
2019-08-23 13:46:31 +00:00
|
|
|
export interface ValidationResult {
|
|
|
|
errors: ValidationMessage[];
|
|
|
|
warnings: ValidationMessage[];
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function validateConfig(
|
|
|
|
config: RenovateConfig,
|
|
|
|
isPreset?: boolean,
|
|
|
|
parentPath?: string
|
|
|
|
): Promise<ValidationResult> {
|
2017-11-03 06:43:26 +00:00
|
|
|
if (!optionTypes) {
|
|
|
|
optionTypes = {};
|
|
|
|
options.forEach(option => {
|
|
|
|
optionTypes[option.name] = option.type;
|
|
|
|
});
|
|
|
|
}
|
2019-08-23 13:46:31 +00:00
|
|
|
let errors: ValidationMessage[] = [];
|
|
|
|
let warnings: ValidationMessage[] = [];
|
2017-07-28 19:15:27 +00:00
|
|
|
|
2019-08-23 13:46:31 +00:00
|
|
|
function getDeprecationMessage(option: string): string {
|
2018-04-17 06:39:26 +00:00
|
|
|
const deprecatedOptions = {
|
|
|
|
branchName: `Direct editing of branchName is now deprecated. Please edit branchPrefix, managerBranchPrefix, 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];
|
|
|
|
}
|
|
|
|
|
2019-08-23 13:46:31 +00:00
|
|
|
function isIgnored(key: string): boolean {
|
2018-01-16 07:02:08 +00:00
|
|
|
const ignoredNodes = [
|
2018-07-18 07:31:55 +00:00
|
|
|
'$schema',
|
2018-03-01 16:35:11 +00:00
|
|
|
'prBanner',
|
2018-01-16 07:02:08 +00:00
|
|
|
'depType',
|
|
|
|
'npmToken',
|
|
|
|
'packageFile',
|
|
|
|
'forkToken',
|
|
|
|
'repository',
|
2018-07-29 06:43:53 +00:00
|
|
|
'vulnerabilityAlertsOnly',
|
2019-10-06 08:53:51 +00:00
|
|
|
'vulnerabilityAlert',
|
2019-04-23 14:07:27 +00:00
|
|
|
'copyLocalLibs', // deprecated - functionality is now enabled by default
|
2018-09-21 03:43:51 +00:00
|
|
|
'prBody', // deprecated
|
2018-01-16 07:02:08 +00:00
|
|
|
];
|
2018-06-04 18:44:32 +00:00
|
|
|
return ignoredNodes.includes(key);
|
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;
|
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({
|
|
|
|
depName: 'Deprecation Warning',
|
|
|
|
message: getDeprecationMessage(key),
|
|
|
|
});
|
|
|
|
}
|
2020-01-27 11:48:08 +00:00
|
|
|
const templateKeys = [
|
|
|
|
'branchName',
|
|
|
|
'commitBody',
|
|
|
|
'commitMessage',
|
|
|
|
'prTitle',
|
|
|
|
'semanticCommitScope',
|
|
|
|
];
|
|
|
|
if (templateKeys.includes(key) && val) {
|
|
|
|
try {
|
|
|
|
let res = handlebars.compile(val)(config);
|
|
|
|
res = handlebars.compile(res)(config);
|
2020-02-24 11:27:10 +00:00
|
|
|
handlebars.compile(res)(config);
|
2020-01-27 11:48:08 +00:00
|
|
|
} catch (err) {
|
|
|
|
errors.push({
|
|
|
|
depName: 'Configuration Error',
|
|
|
|
message: `Invalid handlebars template in config path: ${currentPath}`,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
2017-07-28 19:15:27 +00:00
|
|
|
if (!optionTypes[key]) {
|
|
|
|
errors.push({
|
|
|
|
depName: '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({
|
|
|
|
depName: 'Configuration Error',
|
2018-04-11 19:38:31 +00:00
|
|
|
message: `Invalid ${currentPath}: \`${errorMessage}\``,
|
2017-08-14 09:09:14 +00:00
|
|
|
});
|
|
|
|
}
|
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({
|
|
|
|
depName: 'Configuration Error',
|
2018-04-11 19:38:31 +00:00
|
|
|
message: `${currentPath}: ${errorMessage}`,
|
2018-03-12 03:24:45 +00:00
|
|
|
});
|
|
|
|
}
|
2017-07-29 20:12:19 +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({
|
|
|
|
depName: 'Configuration Error',
|
2018-04-11 19:38:31 +00:00
|
|
|
message: `Configuration option \`${currentPath}\` should be boolean. Found: ${JSON.stringify(
|
2017-08-22 06:12:42 +00:00
|
|
|
val
|
|
|
|
)} (${typeof val})`,
|
2017-07-28 19:15:27 +00:00
|
|
|
});
|
|
|
|
}
|
2019-03-31 06:01:06 +00:00
|
|
|
} else if (type === 'array' && val) {
|
2018-06-04 18:07:22 +00:00
|
|
|
if (!is.array(val)) {
|
2017-07-28 19:15:27 +00:00
|
|
|
errors.push({
|
|
|
|
depName: 'Configuration Error',
|
2018-04-11 19:38:31 +00:00
|
|
|
message: `Configuration option \`${currentPath}\` should be a list (Array)`,
|
2017-07-28 19:15:27 +00:00
|
|
|
});
|
2017-08-02 05:52:28 +00:00
|
|
|
} else {
|
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)) {
|
2018-03-28 08:04:07 +00:00
|
|
|
const subValidation = await module.exports.validateConfig(
|
2018-04-11 19:38:31 +00:00
|
|
|
subval,
|
2018-04-12 10:13:39 +00:00
|
|
|
isPreset,
|
2018-04-11 19:38:31 +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') {
|
2020-02-05 18:17:20 +00:00
|
|
|
const tzRe = /^:timezone\((.+)\)$/;
|
2018-03-28 07:37:19 +00:00
|
|
|
for (const subval of val) {
|
2020-02-05 18:17:20 +00:00
|
|
|
if (is.string(subval) && tzRe.test(subval)) {
|
|
|
|
const [, timezone] = tzRe.exec(subval);
|
2018-03-28 07:37:19 +00:00
|
|
|
const [validTimezone, errorMessage] = hasValidTimezone(
|
|
|
|
timezone
|
|
|
|
);
|
|
|
|
if (!validTimezone) {
|
|
|
|
errors.push({
|
|
|
|
depName: 'Configuration Error',
|
2018-04-11 19:38:31 +00:00
|
|
|
message: `${currentPath}: ${errorMessage}`,
|
2018-03-28 07:37:19 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-04-12 10:13:39 +00:00
|
|
|
|
|
|
|
const selectors = [
|
2018-04-28 06:56:15 +00:00
|
|
|
'paths',
|
2018-12-11 11:55:12 +00:00
|
|
|
'languages',
|
2019-03-11 16:42:30 +00:00
|
|
|
'baseBranchList',
|
2018-12-11 11:37:13 +00:00
|
|
|
'managers',
|
2019-03-12 06:27:49 +00:00
|
|
|
'datasources',
|
2018-04-12 10:13:39 +00:00
|
|
|
'depTypeList',
|
|
|
|
'packageNames',
|
|
|
|
'packagePatterns',
|
|
|
|
'excludePackageNames',
|
|
|
|
'excludePackagePatterns',
|
2018-12-11 11:03:09 +00:00
|
|
|
'sourceUrlPrefixes',
|
2018-07-04 07:30:29 +00:00
|
|
|
'updateTypes',
|
2018-04-12 10:13:39 +00:00
|
|
|
];
|
2018-03-28 08:04:07 +00:00
|
|
|
if (key === 'packageRules') {
|
|
|
|
for (const packageRule of val) {
|
|
|
|
let hasSelector = false;
|
2018-06-04 18:07:22 +00:00
|
|
|
if (is.object(packageRule)) {
|
2020-03-02 11:06:16 +00:00
|
|
|
const resolvedRule = await resolveConfigPresets(
|
|
|
|
packageRule as RenovateConfig
|
|
|
|
);
|
2019-02-20 21:29:38 +00:00
|
|
|
errors.push(
|
|
|
|
...managerValidator.check({ resolvedRule, currentPath })
|
|
|
|
);
|
2018-03-28 08:04:07 +00:00
|
|
|
for (const pKey of Object.keys(resolvedRule)) {
|
|
|
|
if (selectors.includes(pKey)) {
|
|
|
|
hasSelector = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!hasSelector) {
|
2018-04-11 19:38:31 +00:00
|
|
|
const message = `${currentPath}: Each packageRule must contain at least one selector (${selectors.join(
|
2018-03-28 13:13:32 +00:00
|
|
|
', '
|
|
|
|
)}). If you wish for configuration to apply to all packages, it is not necessary to place it inside a packageRule at all.`;
|
|
|
|
errors.push({
|
|
|
|
depName: 'Configuration Error',
|
2018-03-28 08:04:07 +00:00
|
|
|
message,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
errors.push({
|
|
|
|
depName: 'Configuration Error',
|
2018-04-11 19:38:31 +00:00
|
|
|
message: `${currentPath} must contain JSON objects`,
|
2018-03-28 08:04:07 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-10-22 06:48:40 +00:00
|
|
|
if (key === 'packagePatterns' || key === 'excludePackagePatterns') {
|
|
|
|
for (const pattern of val) {
|
|
|
|
if (pattern !== '*') {
|
|
|
|
try {
|
|
|
|
regEx(pattern);
|
|
|
|
} catch (e) {
|
2019-10-15 08:14:49 +00:00
|
|
|
errors.push({
|
|
|
|
depName: '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') {
|
|
|
|
for (const fileMatch of val) {
|
|
|
|
try {
|
|
|
|
regEx(fileMatch);
|
|
|
|
} catch (e) {
|
|
|
|
errors.push({
|
|
|
|
depName: 'Configuration Error',
|
|
|
|
message: `Invalid regExp for ${currentPath}: \`${fileMatch}\``,
|
|
|
|
});
|
|
|
|
}
|
2018-04-30 11:18:51 +00:00
|
|
|
}
|
|
|
|
}
|
2018-04-12 10:13:39 +00:00
|
|
|
if (
|
2018-04-27 03:45:22 +00:00
|
|
|
(selectors.includes(key) || key === 'matchCurrentVersion') &&
|
2020-02-05 18:17:20 +00:00
|
|
|
!/p.*Rules\[\d+\]$/.test(parentPath) && // Inside a packageRule
|
2018-04-12 10:13:39 +00:00
|
|
|
(parentPath || !isPreset) // top level in a preset
|
|
|
|
) {
|
|
|
|
errors.push({
|
|
|
|
depName: 'Configuration Error',
|
|
|
|
message: `${currentPath}: ${key} should be inside a \`packageRule\` only`,
|
|
|
|
});
|
|
|
|
}
|
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({
|
|
|
|
depName: '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
|
|
|
});
|
|
|
|
}
|
2019-11-27 05:48:49 +00:00
|
|
|
} else if (type === 'object' && currentPath !== 'compatibility') {
|
2018-06-04 18:07:22 +00:00
|
|
|
if (is.object(val)) {
|
2019-03-31 07:16:29 +00:00
|
|
|
const ignoredObjects = options
|
|
|
|
.filter(option => option.freeChoice)
|
|
|
|
.map(option => option.name);
|
|
|
|
if (!ignoredObjects.includes(key)) {
|
|
|
|
const subValidation = await module.exports.validateConfig(
|
|
|
|
val,
|
|
|
|
isPreset,
|
|
|
|
currentPath
|
|
|
|
);
|
|
|
|
warnings = warnings.concat(subValidation.warnings);
|
|
|
|
errors = errors.concat(subValidation.errors);
|
|
|
|
}
|
2017-07-28 19:15:27 +00:00
|
|
|
} else {
|
|
|
|
errors.push({
|
|
|
|
depName: '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
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-08-23 13:46:31 +00:00
|
|
|
function sortAll(a: ValidationMessage, b: ValidationMessage): number {
|
2018-04-28 06:48:12 +00:00
|
|
|
if (a.depName === b.depName) {
|
2019-04-16 14:03:37 +00:00
|
|
|
return a.message > b.message ? 1 : -1;
|
2018-04-28 06:48:12 +00:00
|
|
|
}
|
|
|
|
// istanbul ignore next
|
2019-04-16 14:03:37 +00:00
|
|
|
return a.depName > b.depName ? 1 : -1;
|
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
|
|
|
}
|