UNPKG

renovate

Version:

Automated dependency updates. Flexible so you don't need to be.

742 lines (741 loc) • 31.4 kB
import { regEx } from "../util/regex.js"; import { getConfigFileNames } from "./app-strings.js"; import { GlobalConfig } from "./global.js"; import { anyMatchRegexOrGlobList, getRegexPredicate, isRegexMatch, matchRegexOrGlobList } from "../util/string-match.js"; import { logger } from "../logger/index.js"; import { AllManagersListLiteral, CustomManagersListLiteral } from "../manager-list.generated.js"; import { AllVersioningsListLiteral } from "../versioning-list.generated.js"; import { supportedDatasources } from "./presets/internal/merge-confidence.preset.js"; import { allowedStatusCheckStrings } from "./types.js"; import { getOptions } from "./options/index.js"; import { parseUrl } from "../util/url.js"; import { migrateConfig } from "./migration.js"; import { validate } from "../util/template/index.js"; import { isCustomManager } from "../modules/manager/custom/index.js"; import { packageCacheNamespaces } from "../util/cache/package/namespaces.js"; import { getToolConfig } from "../util/exec/containerbase.js"; import { isConstraintName, isToolName } from "../util/exec/types.js"; import { getExpression } from "../util/jsonata.js"; import { hasValidSchedule, hasValidTimezone } from "../workers/repository/update/branch/schedule.js"; import { parsePreset } from "./presets/parse.js"; import { resolveConfigPresets } from "./presets/index.js"; import { check } from "./validation-helpers/match-base-branches.js"; import { check as check$1 } from "./validation-helpers/regex-glob-matchers.js"; import { getParentName, isFalseGlobal, validateJSONataManagerFields, validateNumber, validatePlainObject, validateRegexManagerFields } from "./validation-helpers/utils.js"; import is, { isArray, isEmptyString, isNonEmptyArray, isNonEmptyString, isObject, isPlainObject, isString, isUndefined } from "@sindresorhus/is"; //#region lib/config/validation.ts const options = getOptions(); let optionsInitialized = false; let optionTypes; let optionParents; let optionGlobals; let optionInherits; let optionRegexOrGlob; let optionAllowsNegativeIntegers; let optionSupportsTemplating; const managerList = AllManagersListLiteral; const allManagersList = [...AllManagersListLiteral, ...CustomManagersListLiteral]; const topLevelObjects = [...managerList, "env"]; const ignoredNodes = [ "$schema", "headers", "depType", "npmToken", "packageFile", "forkToken", "repository", "vulnerabilityAlertsOnly", "vulnerabilityAlert", "isVulnerabilityAlert", "vulnerabilityFixVersion", "copyLocalLibs", "prBody", "minimumConfidence" ]; const tzRe = regEx(/^:timezone\((.+)\)$/); const rulesRe = regEx(/p.*Rules\[\d+\]$/); function isIgnored(key) { return ignoredNodes.includes(key); } function getUnsupportedEnabledManagers(enabledManagers) { return enabledManagers.filter((manager) => !allManagersList.includes(manager.replace("custom.", ""))); } function getDeprecationMessage(option) { const deprecatedOptions = { 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.` }; if (deprecatedOptions[option]) return deprecatedOptions[option]; const found = options.find((o) => o.name === option); if (!found) return; if (!found.deprecationMsg) return; return `The '${option}' option is deprecated: ${found.deprecationMsg}`; } function isInhertConfigOption(key) { return optionInherits.has(key); } function isRegexOrGlobOption(key) { return optionRegexOrGlob.has(key); } function isGlobalOption(key) { return optionGlobals.has(key); } function initOptions() { if (optionsInitialized) return; optionParents = {}; optionInherits = /* @__PURE__ */ new Set(); optionTypes = {}; optionRegexOrGlob = /* @__PURE__ */ new Set(); optionGlobals = /* @__PURE__ */ new Set(); optionAllowsNegativeIntegers = /* @__PURE__ */ new Set(); optionSupportsTemplating = /* @__PURE__ */ new Set(); for (const option of options) { optionTypes[option.name] = option.type; if (option.parents) optionParents[option.name] = option.parents; if (option.inheritConfigSupport) optionInherits.add(option.name); if (option.patternMatch) optionRegexOrGlob.add(option.name); if (option.globalOnly) optionGlobals.add(option.name); if (option.allowNegative) optionAllowsNegativeIntegers.add(option.name); if (option.supportsTemplating) optionSupportsTemplating.add(option.name); } optionsInitialized = true; } async function validateConfig(configType, config, isPreset, parentPath) { initOptions(); let errors = []; let warnings = []; for (const [key, val] of Object.entries(config)) { const currentPath = parentPath ? `${parentPath}.${key}` : key; /* v8 ignore next 7 -- TODO: add test */ if (key === "__proto__") { errors.push({ topic: "Config security error", message: "__proto__" }); continue; } if (parentPath && parentPath !== "onboardingConfig" && 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}"` }); if (isGlobalOption(key)) { if (configType === "global") { await validateGlobalConfig(key, val, optionTypes[key], warnings, errors, currentPath, config); continue; } else if (!isFalseGlobal(key, parentPath) && !(configType === "inherit" && isInhertConfigOption(key))) { warnings.push({ topic: "Configuration Error", message: `The "${key}" option is a global option reserved only for Renovate's global configuration and cannot be configured within a repository's config file.` }); continue; } } if (key === "enabledManagers" && val) { const unsupportedManagers = getUnsupportedEnabledManagers(val); if (isNonEmptyArray(unsupportedManagers)) errors.push({ topic: "Configuration Error", message: `The following managers configured in enabledManagers are not supported: "${unsupportedManagers.join(", ")}"` }); } if (key === "registryUrls" && !parentPath && isNonEmptyArray(val)) warnings.push({ topic: "Configuration Warning", message: "Setting `registryUrls` at the top level of your config will apply it to all managers and datasources, which can cause the wrong registry URL to be used for some packages. Use `registryUrls` inside `packageRules` to target specific managers or packages." }); if (key === "defaultRegistryUrls" && !parentPath && isNonEmptyArray(val)) warnings.push({ topic: "Configuration Warning", message: "Setting `defaultRegistryUrls` at the top level of your config will apply it to all managers and datasources, which can cause the wrong registry URL to be used for some packages. Use `defaultRegistryUrls` inside `packageRules` to target specific managers or packages." }); if (!isIgnored(key) && !is.function(val)) { if (getDeprecationMessage(key)) warnings.push({ topic: "Deprecation Warning", message: getDeprecationMessage(key) }); if (optionSupportsTemplating.has(key) && val) try { validate(val.toString()); } catch { errors.push({ topic: "Configuration Error", message: `Invalid template in config path: ${currentPath}` }); } const parentName = getParentName(parentPath); if (!isPreset && optionParents[key] && !optionParents[key].includes(parentName)) { const message = `"${key}" can't be used in "${parentName}". Allowed objects: ${optionParents[key]?.toSorted().join(", ")}.`; warnings.push({ topic: `${parentPath ? `${parentPath}.` : ""}${key}`, message }); } // v8 ignore else -- intentionally unhandled - if we knew what was to be covered here, we'd add validation if (!optionTypes[key]) errors.push({ topic: "Configuration Error", message: `Invalid configuration option: ${currentPath}` }); else if (key === "schedule") { const [validSchedule, errorMessage] = hasValidSchedule(val); if (!validSchedule) errors.push({ topic: "Configuration Error", message: `Invalid ${currentPath}: \`${errorMessage}\`` }); } else if ([ "allowedVersions", "matchCurrentVersion", "matchCurrentValue", "matchNewValue" ].includes(key) && isRegexMatch(val)) { if (!getRegexPredicate(val)) errors.push({ topic: "Configuration Error", message: `Invalid regExp for ${currentPath}: \`${val}\`` }); } else if (key === "timezone" && val !== null) { const [validTimezone, errorMessage] = hasValidTimezone(val); if (!validTimezone) errors.push({ topic: "Configuration Error", message: `${currentPath}: ${errorMessage}` }); } else if (val !== null) { const type = optionTypes[key]; if (type === "boolean") { if (val !== true && val !== false) errors.push({ topic: "Configuration Error", message: `Configuration option \`${currentPath}\` should be boolean. Found: ${JSON.stringify(val)} (${typeof val})` }); } else if (type === "integer") { const allowsNegative = optionAllowsNegativeIntegers.has(key); errors.push(...validateNumber(key, val, allowsNegative, currentPath)); } else if (type === "array" && val) if (isArray(val)) { for (const [subIndex, subval] of val.entries()) if (isObject(subval)) { const subValidation = await validateConfig(configType, subval, isPreset, `${currentPath}[${subIndex}]`); warnings = warnings.concat(subValidation.warnings); errors = errors.concat(subValidation.errors); } if (isRegexOrGlobOption(key)) errors.push(...check$1({ val, currentPath })); if (key === "extends") for (const subval of val) if (isString(subval)) { if (configType !== "global" && subval.startsWith("global:")) errors.push({ topic: "Configuration Error", message: `${currentPath}: you cannot extend from "global:" presets in a repository config's "extends"` }); if (parentName === "packageRules" && subval.startsWith("group:")) warnings.push({ topic: "Configuration Warning", message: `${currentPath}: you should not extend "group:" presets` }); if (tzRe.test(subval)) { const [, timezone] = tzRe.exec(subval); const [validTimezone, errorMessage] = hasValidTimezone(timezone); if (!validTimezone) errors.push({ topic: "Configuration Error", message: `${currentPath}: ${errorMessage}` }); } if (!subval.includes("{{")) try { parsePreset(subval); } catch { errors.push({ topic: "Configuration Error", message: `${currentPath}: preset "${subval}" is not valid` }); } } else errors.push({ topic: "Configuration Error", message: `${currentPath}: preset value is not a string` }); const selectors = [ "matchFileNames", "matchLanguages", "matchCategories", "matchBaseBranches", "matchManagers", "matchDatasources", "matchDepTypes", "matchDepNames", "matchPackageNames", "matchCurrentValue", "matchCurrentVersion", "matchSourceUrls", "matchRegistryUrls", "matchUpdateTypes", "matchConfidence", "matchCurrentAge", "matchRepositories", "matchNewValue", "matchJsonata" ]; if (key === "packageRules") for (const [subIndex, packageRule] of val.entries()) if (isObject(packageRule)) { const { config: resolved } = await resolveConfigPresets(packageRule, config); const resolvedRule = migrateConfig({ packageRules: [resolved] }).migratedConfig.packageRules[0]; warnings.push(...check({ resolvedRule, currentPath: `${currentPath}[${subIndex}]`, baseBranchPatterns: config.baseBranchPatterns })); const selectorLength = Object.keys(resolvedRule).filter((ruleKey) => selectors.includes(ruleKey)).length; if (!selectorLength) { const message = `${currentPath}[${subIndex}]: Each packageRule must contain at least one match* or exclude* selector. Rule: ${JSON.stringify(packageRule)}`; errors.push({ topic: "Configuration Error", message }); } 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(packageRule)}`; warnings.push({ topic: "Configuration Error", message }); } const preLookupOptions = [ "allowedVersions", "extractVersion", "followTag", "ignoreDeps", "ignoreUnstable", "rangeStrategy", "registryUrls", "respectLatest", "rollbackPrs", "separateMajorMinor", "separateMinorPatch", "separateMultipleMajor", "separateMultipleMinor", "versioning" ]; if (isNonEmptyArray(resolvedRule.matchUpdateTypes)) { for (const option of preLookupOptions) if (resolvedRule[option] !== void 0) { const message = `${currentPath}[${subIndex}]: packageRules cannot combine both matchUpdateTypes and ${option}. Rule: ${JSON.stringify(packageRule)}`; errors.push({ topic: "Configuration Error", message }); } } } else errors.push({ topic: "Configuration Error", message: `${currentPath} must contain JSON objects` }); if (key === "customManagers") { const allowedKeys = [ "customType", "description", "fileFormat", "managerFilePatterns", "matchStrings", "matchStringsStrategy", "depNameTemplate", "packageNameTemplate", "datasourceTemplate", "versioningTemplate", "registryUrlTemplate", "currentValueTemplate", "extractVersionTemplate", "autoReplaceStringTemplate", "depTypeTemplate" ]; for (const customManager of val) if (Object.keys(customManager).some((k) => !allowedKeys.includes(k))) { const disallowedKeys = Object.keys(customManager).filter((k) => !allowedKeys.includes(k)); errors.push({ topic: "Configuration Error", message: `Custom Manager contains disallowed fields: ${disallowedKeys.join(", ")}` }); } else if (isNonEmptyString(customManager.customType) && isCustomManager(customManager.customType)) if (isNonEmptyArray(customManager.managerFilePatterns)) switch (customManager.customType) { case "regex": validateRegexManagerFields(customManager, currentPath, errors); break; case "jsonata": validateJSONataManagerFields(customManager, currentPath, errors); break; } else errors.push({ topic: "Configuration Error", message: `Each Custom Manager must contain a non-empty managerFilePatterns array` }); else if (isEmptyString(customManager.customType) || isUndefined(customManager.customType)) errors.push({ topic: "Configuration Error", message: `Each Custom Manager must contain a non-empty customType string` }); else errors.push({ topic: "Configuration Error", message: `Invalid customType: ${customManager.customType}. Key is not a custom manager` }); } if (["matchPackageNames", "matchDepNames"].includes(key)) { const startPattern = regEx(/!?\//); const endPattern = regEx(/\/g?i?$/); for (const pattern of val) if (startPattern.test(pattern) && endPattern.test(pattern)) try { regEx(pattern.replace(startPattern, "/")); } catch { errors.push({ topic: "Configuration Error", message: `Invalid regExp for ${currentPath}: \`${pattern}\`` }); } } if (key === "baseBranchPatterns") { for (const baseBranchPattern of val) if (isRegexMatch(baseBranchPattern) && !getRegexPredicate(baseBranchPattern)) errors.push({ topic: "Configuration Error", message: `Invalid regExp for ${currentPath}: \`${baseBranchPattern}\`` }); } if ((selectors.includes(key) || key === "matchCurrentVersion" || key === "matchCurrentValue") && !rulesRe.test(parentPath) && (isString(parentPath) || !isPreset)) errors.push({ topic: "Configuration Error", message: `${currentPath}: ${key} should be inside a \`packageRule\` only` }); } else errors.push({ topic: "Configuration Error", message: `Configuration option \`${currentPath}\` should be a list (Array)` }); else if (type === "string") { if (!isString(val)) errors.push({ topic: "Configuration Error", message: `Configuration option \`${currentPath}\` should be a string` }); } else if (type === "object") if (isPlainObject(val)) { if (key === "registryAliases") { const res = validatePlainObject(val); if (res !== true) errors.push({ topic: "Configuration Error", message: `Invalid \`${currentPath}.${key}.${res}\` configuration: value is not a string` }); } else if (key === "env") { const allowedEnvVars = configType === "global" ? config.allowedEnv ?? [] : GlobalConfig.get("allowedEnv"); for (const [envVarName, envVarValue] of Object.entries(val)) { if (!isString(envVarValue)) errors.push({ topic: "Configuration Error", message: `Invalid env variable value: \`${currentPath}.${envVarName}\` must be a string.` }); if (!matchRegexOrGlobList(envVarName, allowedEnvVars)) errors.push({ topic: "Configuration Error", message: `Env variable name \`${envVarName}\` is not allowed by this bot's \`allowedEnv\`.` }); } } else if (key === "statusCheckNames") for (const [statusCheckKey, statusCheckValue] of Object.entries(val)) { if (!allowedStatusCheckStrings.includes(statusCheckKey)) errors.push({ topic: "Configuration Error", message: `Invalid \`${currentPath}.${key}.${statusCheckKey}\` configuration: key is not allowed.` }); if (!(isString(statusCheckValue) || null === statusCheckValue)) { errors.push({ topic: "Configuration Error", message: `Invalid \`${currentPath}.${statusCheckKey}\` configuration: status check is not a string.` }); continue; } } else if (key === "customDatasources") { const allowedKeys = [ "description", "defaultRegistryUrlTemplate", "format", "transformTemplates" ]; for (const [customDatasourceName, customDatasourceValue] of Object.entries(val)) { if (!isPlainObject(customDatasourceValue)) { errors.push({ topic: "Configuration Error", message: `Invalid \`${currentPath}.${customDatasourceName}\` configuration: customDatasource is not an object` }); continue; } for (const [subKey, subValue] of Object.entries(customDatasourceValue)) if (!allowedKeys.includes(subKey)) errors.push({ topic: "Configuration Error", message: `Invalid \`${currentPath}.${subKey}\` configuration: key is not allowed` }); else if (subKey === "transformTemplates") { if (!isArray(subValue, isString)) errors.push({ topic: "Configuration Error", message: `Invalid \`${currentPath}.${subKey}\` configuration: is not an array of string` }); } else if (subKey === "description") { if (!(isString(subValue) || isArray(subValue, isString))) errors.push({ topic: "Configuration Error", message: `Invalid \`${currentPath}.${subKey}\` configuration: is not an array of strings` }); } else if (!isString(subValue)) errors.push({ topic: "Configuration Error", message: `Invalid \`${currentPath}.${subKey}\` configuration: is a string` }); } } else if (key === "installTools") { for (const toolName of Object.keys(val)) if (!isToolName(toolName)) warnings.push({ topic: "Configuration Error", message: `Invalid \`${currentPath}.${toolName}\` configuration: not a valid tool name.` }); } else if (key === "constraints") { const { get: getVersioning } = await import("../modules/versioning/index.js"); for (const [k, v] of Object.entries(val)) { if (!isString(v)) { errors.push({ topic: "Configuration Error", message: `Configuration option \`${currentPath}.${k}\` should be an object of key-value pairs of constraints and their value` }); break; } if (!isConstraintName(k)) warnings.push({ topic: "Configuration Error", message: `Configuration option \`${currentPath}.${k}\`: \`${k}\` is not a supported constraint name` }); else if (isToolName(k)) { const versioningId = getToolConfig(k).versioning; if (!getVersioning(versioningId).isValid(v)) warnings.push({ topic: "Configuration Error", message: `Configuration option \`${currentPath}.${k}=${v}\` is not a valid tool version constraint, according to \`${versioningId}\` versioning` }); } } } else if (key === "constraintsVersioning") for (const [k, v] of Object.entries(val)) { if (!isString(v)) { errors.push({ topic: "Configuration Error", message: `Configuration option \`${currentPath}.${k}\` should be an object of key-value pairs of additional constraint names and their versioning` }); break; } if (isToolName(k)) errors.push({ topic: "Configuration Error", message: `Configuration option \`${currentPath}.${k}\` is not a valid additional constraint name, as \`${k}\` is a tool name, and \`constraintsVersioning\` can only override the versioning for a non-tool constraint` }); else if (isConstraintName(k)) { const versioningName = v.split(":")[0]; if (!AllVersioningsListLiteral.includes(versioningName)) errors.push({ topic: "Configuration Error", message: `Configuration option \`${currentPath}.${k}=${v}\`: \`${v}\` is not a valid versioning scheme` }); } else errors.push({ topic: "Configuration Error", message: `Configuration option \`${currentPath}.${k}\`: \`${k}\` is not a known additional constraint name` }); } else if (!options.filter((option) => option.freeChoice).map((option) => option.name).includes(key)) { const subValidation = await validateConfig(configType, val, isPreset, currentPath); warnings = warnings.concat(subValidation.warnings); errors = errors.concat(subValidation.errors); } } else errors.push({ topic: "Configuration Error", message: `Configuration option \`${currentPath}\` should be a json object` }); else // v8 ignore next -- intentionally unhandled - if we knew what was to be covered here, we'd add validation logger.debug({}, `Unhandled validation for ${type} at \`${currentPath}\``); } } if (key === "hostRules" && isArray(val)) { const allowedHeaders = configType === "global" ? config.allowedHeaders ?? [] : GlobalConfig.get("allowedHeaders"); for (const rule of val) { if (isNonEmptyString(rule.matchHost)) { if (rule.matchHost.includes("://")) { if (parseUrl(rule.matchHost) === null) errors.push({ topic: "Configuration Error", message: `hostRules matchHost \`${rule.matchHost}\` is not a valid URL.` }); } } else if (isEmptyString(rule.matchHost)) errors.push({ topic: "Configuration Error", message: "Invalid value for hostRules matchHost. It cannot be an empty string." }); if (!rule.headers) continue; for (const [header, value] of Object.entries(rule.headers)) { if (!isString(value)) errors.push({ topic: "Configuration Error", message: `Invalid hostRules headers value configuration: header must be a string.` }); if (!matchRegexOrGlobList(header, allowedHeaders)) errors.push({ topic: "Configuration Error", message: `hostRules header \`${header}\` is not allowed by this bot's \`allowedHeaders\`.` }); } } } if (key === "matchJsonata" && isArray(val, isString)) for (const expression of val) { const res = getExpression(expression); if (res instanceof Error) errors.push({ topic: "Configuration Error", message: `Invalid JSONata expression for ${currentPath}: ${res.message}` }); } } function sortAll(a, b) { if (a.topic === b.topic) return a.message > b.message ? 1 : -1; return a.topic > b.topic ? 1 : -1; } errors.sort(sortAll); warnings.sort(sortAll); return { errors, warnings }; } /** * Basic validation for global config options */ async function validateGlobalConfig(key, val, type, warnings, errors, currentPath, config) { /* v8 ignore next 5 -- not testable yet */ if (getDeprecationMessage(key)) warnings.push({ topic: "Deprecation Warning", message: getDeprecationMessage(key) }); if (key === "binarySource" && val === "docker") warnings.push({ topic: "Deprecation Warning", message: "Usage of `binarySource=docker` is deprecated, and will be removed in the future. Please migrate to `binarySource=install`. Feedback on the usage of `binarySource=docker` is welcome at https://github.com/renovatebot/renovate/discussions/40742" }); if (val !== null) { // v8 ignore else -- TODO: add test #40625 if (type === "string") if (isString(val)) { if (key === "onboardingConfigFileName" && !getPossibleConfigFileNames({ configFileNames: config.configFileNames, platform: config.platform }).includes(val)) warnings.push({ topic: "Configuration Error", message: `Invalid value \`${val}\` for \`${currentPath}\`. The allowed values are ${getPossibleConfigFileNames({ configFileNames: config.configFileNames, platform: config.platform }).join(", ")}.` }); else if (key === "repositoryCache" && ![ "enabled", "disabled", "reset" ].includes(val)) warnings.push({ topic: "Configuration Error", message: `Invalid value \`${val}\` for \`${currentPath}\`. The allowed values are ${[ "enabled", "disabled", "reset" ].join(", ")}.` }); else if (key === "dryRun" && ![ "extract", "lookup", "full" ].includes(val)) warnings.push({ topic: "Configuration Error", message: `Invalid value \`${val}\` for \`${currentPath}\`. The allowed values are ${[ "extract", "lookup", "full" ].join(", ")}.` }); else if (key === "binarySource" && ![ "docker", "global", "install", "hermit" ].includes(val)) warnings.push({ topic: "Configuration Error", message: `Invalid value \`${val}\` for \`${currentPath}\`. The allowed values are ${[ "docker", "global", "install", "hermit" ].join(", ")}.` }); else if (key === "requireConfig" && ![ "required", "optional", "ignored" ].includes(val)) warnings.push({ topic: "Configuration Error", message: `Invalid value \`${val}\` for \`${currentPath}\`. The allowed values are ${[ "required", "optional", "ignored" ].join(", ")}.` }); else if (key === "gitUrl" && ![ "default", "ssh", "endpoint" ].includes(val)) warnings.push({ topic: "Configuration Error", message: `Invalid value \`${val}\` for \`${currentPath}\`. The allowed values are ${[ "default", "ssh", "endpoint" ].join(", ")}.` }); if (key === "reportType" && ["s3", "file"].includes(val) && !isString(config.reportPath)) errors.push({ topic: "Configuration Error", message: `reportType '${val}' requires a configured reportPath` }); } else warnings.push({ topic: "Configuration Error", message: `Configuration option \`${currentPath}\` should be a string.` }); else if (type === "integer") { const allowsNegative = optionAllowsNegativeIntegers.has(key); warnings.push(...validateNumber(key, val, allowsNegative, currentPath)); } else if (type === "boolean") { if (val !== true && val !== false) warnings.push({ topic: "Configuration Error", message: `Configuration option \`${currentPath}\` should be a boolean. Found: ${JSON.stringify(val)} (${typeof val}).` }); } else if (type === "array") if (isArray(val)) { for (const [subIndex, subval] of val.entries()) if (isObject(subval)) { const subValidation = await validateConfig("global", subval, false, `${currentPath}[${subIndex}]`); warnings.push(...subValidation.warnings); errors.push(...subValidation.errors); } if (isRegexOrGlobOption(key)) warnings.push(...check$1({ val, currentPath })); if (key === "gitNoVerify") { const allowedValues = ["commit", "push"]; for (const value of val) // v8 ignore else -- TODO: add test #40625 if (!allowedValues.includes(value)) warnings.push({ topic: "Configuration Error", message: `Invalid value for \`${currentPath}\`. The allowed values are ${allowedValues.join(", ")}.` }); } if (key === "mergeConfidenceDatasources") { const allowedValues = supportedDatasources; for (const value of val) // v8 ignore else -- TODO: add test #40625 if (!allowedValues.includes(value)) warnings.push({ topic: "Configuration Error", message: `Invalid value \`${value}\` for \`${currentPath}\`. The allowed values are ${allowedValues.join(", ")}.` }); } } else warnings.push({ topic: "Configuration Error", message: `Configuration option \`${currentPath}\` should be a list (Array).` }); else if (type === "object") if (isPlainObject(val)) if (key === "onboardingConfig") { const subValidation = await validateConfig("repo", val); for (const warning of subValidation.warnings.concat(subValidation.errors)) warnings.push(warning); } else if (key === "force") { const subValidation = await validateConfig("global", val); for (const warning of subValidation.warnings.concat(subValidation.errors)) warnings.push(warning); } else if (key === "cacheTtlOverride") for (const [subKey, subValue] of Object.entries(val)) { const allowsNegative = optionAllowsNegativeIntegers.has(key); warnings.push(...validateNumber(key, subValue, allowsNegative, currentPath, subKey)); if (!packageCacheNamespaces.includes(subKey) && !anyMatchRegexOrGlobList(packageCacheNamespaces, [subKey])) errors.push({ message: `${currentPath}: namespace \`${subKey}\` does not exist`, topic: "Configuration Error" }); } else { const res = validatePlainObject(val); if (res !== true) warnings.push({ topic: "Configuration Error", message: `Invalid \`${currentPath}.${res}\` configuration: value must be a string.` }); } else warnings.push({ topic: "Configuration Error", message: `Configuration option \`${currentPath}\` should be a JSON object.` }); } } function getPossibleConfigFileNames({ configFileNames, platform }) { const filenames = getConfigFileNames(platform); if (isNonEmptyArray(configFileNames)) return filenames.concat(configFileNames); return filenames; } //#endregion export { validateConfig }; //# sourceMappingURL=validation.js.map