UNPKG

renovate

Version:

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

319 lines (316 loc) • 13.9 kB
import { CONFIG_VALIDATION, REPOSITORY_CHANGED } from "../../../constants/error-messages.js"; import { pkg } from "../../../expose.js"; import { setUserEnv } from "../../../util/env.js"; import { regEx } from "../../../util/regex.js"; import { getConfigFileNames } from "../../../config/app-strings.js"; import { coerceString } from "../../../util/string.js"; import { logger } from "../../../logger/index.js"; import { clone } from "../../../util/clone.js"; import { mergeChildConfig } from "../../../config/utils.js"; import { migrateConfig } from "../../../config/migration.js"; import { add } from "../../../util/host-rules.js"; import { getInheritedOrGlobal, parseJson } from "../../../util/common.js"; import { coerceArray } from "../../../util/array.js"; import { ExternalHostError } from "../../../types/errors/external-host-error.js"; import { resolveConfigPresets } from "../../../config/presets/index.js"; import { validateConfig } from "../../../config/validation.js"; import { readLocalFile, readSystemFile } from "../../../util/fs/index.js"; import { decryptConfig } from "../../../config/decrypt.js"; import { applySecretsAndVariablesToConfig } from "../../../config/secrets.js"; import { clear } from "../../../util/http/queue.js"; import { clear as clear$1 } from "../../../util/http/throttle.js"; import { getCache } from "../../../util/cache/repository/index.js"; import { maskToken } from "../../../util/mask.js"; import { coerceObject } from "../../../util/object.js"; import { setNpmrc } from "../../../modules/datasource/npm/npmrc.js"; import "../../../modules/datasource/npm/index.js"; import { scm } from "../../../modules/platform/scm.js"; import { platform } from "../../../modules/platform/index.js"; import "../../../config/index.js"; import { migrateAndValidate } from "../../../config/migrate-validate.js"; import { parseFileConfig } from "../../../config/parse.js"; import { getOnboardingConfig } from "../onboarding/branch/config.js"; import { getOnboardingConfigFromCache, getOnboardingFileNameFromCache, setOnboardingConfigDetails } from "../onboarding/branch/onboarding-branch-cache.js"; import { OnboardingState, getDefaultConfigFileName } from "../onboarding/common.js"; import { isNonEmptyArray, isNonEmptyObject, isNonEmptyString, isString } from "@sindresorhus/is"; //#region lib/workers/repository/init/merge.ts async function detectConfigFile() { const fileList = await scm.getFileList(); for (const fileName of getConfigFileNames()) if (fileName === "package.json") try { if (JSON.parse(await readLocalFile("package.json", "utf8")).renovate) { logger.warn("Using package.json for Renovate config is deprecated - please use a dedicated configuration file instead"); return "package.json"; } } catch {} else if (fileList.includes(fileName)) return fileName; return null; } async function detectRepoFileConfig(branchName) { const cache = getCache(); let { configFileName } = cache; if (isNonEmptyString(configFileName)) { let configFileRaw; try { configFileRaw = await platform.getRawFile(configFileName, void 0, branchName); } catch (err) { // istanbul ignore if if (err instanceof ExternalHostError) throw err; configFileRaw = null; } if (configFileRaw) { let configFileParsed = parseJson(configFileRaw, configFileName); if (configFileName === "package.json") configFileParsed = configFileParsed.renovate; return { configFileName, configFileParsed }; } else { logger.debug("Existing config file no longer exists"); delete cache.configFileName; } } if (OnboardingState.onboardingCacheValid) configFileName = getOnboardingFileNameFromCache(); else configFileName = coerceString(await detectConfigFile()); if (!configFileName) { logger.debug("No renovate config file found"); cache.configFileName = ""; return {}; } cache.configFileName = configFileName; logger.debug(`Found ${configFileName} config file`); let configFileParsed; let configFileRaw; if (OnboardingState.onboardingCacheValid) { const cachedConfig = getOnboardingConfigFromCache(); const parsedConfig = cachedConfig ? JSON.parse(cachedConfig) : void 0; if (parsedConfig) { setOnboardingConfigDetails(configFileName, JSON.stringify(parsedConfig)); return { configFileName, configFileParsed: parsedConfig }; } } if (configFileName === "package.json") { configFileParsed = JSON.parse(await readLocalFile("package.json", "utf8")).renovate; if (isString(configFileParsed)) { logger.debug("Massaging string renovate config to extends array"); configFileParsed = { extends: [configFileParsed] }; } logger.debug({ config: configFileParsed }, "package.json>renovate config"); } else { configFileRaw = await readLocalFile(configFileName, "utf8"); // istanbul ignore if if (!isString(configFileRaw)) { logger.warn({ configFileName }, "Null contents when reading config file"); throw new Error(REPOSITORY_CHANGED); } // istanbul ignore if if (!configFileRaw.length) configFileRaw = "{}"; const parseResult = parseFileConfig(configFileName, configFileRaw); if (!parseResult.success) return { configFileName, configFileParseError: { validationError: parseResult.validationError, validationMessage: parseResult.validationMessage } }; configFileParsed = parseResult.parsedContents; logger.debug({ fileName: configFileName, config: configFileParsed }, "Repository config"); } setOnboardingConfigDetails(configFileName, JSON.stringify(configFileParsed)); return { configFileName, configFileParsed }; } function checkForRepoConfigError(repoConfig) { if (!repoConfig.configFileParseError) return; const error = new Error(CONFIG_VALIDATION); error.validationSource = repoConfig.configFileName; error.validationError = repoConfig.configFileParseError.validationError; error.validationMessage = repoConfig.configFileParseError.validationMessage; throw error; } async function mergeRenovateConfig(config, branchName) { let returnConfig = { ...config }; let repoConfig = {}; if (getInheritedOrGlobal("requireConfig") !== "ignored") repoConfig = await detectRepoFileConfig(branchName); if (!repoConfig.configFileParsed && config.mode === "silent") { logger.debug("When mode=silent and repo has no config file, we use the onboarding config as repo config"); repoConfig = { configFileName: getDefaultConfigFileName(), configFileParsed: await getOnboardingConfig(config) }; } const resolvedRepoConfig = await resolveStaticRepoConfig(coerceObject(repoConfig?.configFileParsed), process.env.RENOVATE_X_STATIC_REPO_CONFIG_FILE); const repoEntryConfig = returnConfig.repositoryEntryConfig; delete returnConfig.repositoryEntryConfig; if (isNonEmptyObject(repoEntryConfig)) { const repoEntry = repoEntryConfig; const toResolve = { ...repoEntry, extends: [...coerceArray(returnConfig.extends), ...coerceArray(repoEntry.extends)], ignorePresets: [ ...coerceArray(returnConfig.ignorePresets), ...coerceArray(repoEntry.ignorePresets), ...coerceArray(resolvedRepoConfig.ignorePresets) ] }; delete returnConfig.extends; const { config: resolvedRepoEntry } = await resolveConfigPresets(toResolve, config); applyNpmrc(resolvedRepoEntry, "resolvedRepoEntry"); const resolvedRepoEntryWithSecrets = applySecretsAndVariablesToConfig({ config: resolvedRepoEntry, secrets: config.secrets, variables: config.variables }); applyHostRules(resolvedRepoEntryWithSecrets); returnConfig = mergeChildConfig(returnConfig, resolvedRepoEntryWithSecrets); } if (isNonEmptyArray(returnConfig.extends)) { resolvedRepoConfig.extends = [...coerceArray(returnConfig.extends), ...coerceArray(resolvedRepoConfig.extends)]; delete returnConfig.extends; } checkForRepoConfigError(repoConfig); const migratedConfig = await migrateAndValidate(config, resolvedRepoConfig); if (migratedConfig.errors?.length) { const error = new Error(CONFIG_VALIDATION); error.validationSource = repoConfig.configFileName; error.validationError = "The renovate configuration file contains some invalid settings"; error.validationMessage = migratedConfig.errors.map((e) => e.message).join(", "); throw error; } if (migratedConfig.warnings) returnConfig.warnings = [...coerceArray(returnConfig.warnings), ...migratedConfig.warnings]; delete migratedConfig.errors; delete migratedConfig.warnings; const repository = config.repository; const decryptedConfig = await decryptConfig(migratedConfig, repository); applyNpmrc(decryptedConfig, "decrypted"); await logShallowConfig(decryptedConfig, config); const { config: configToDecrypt } = await resolveConfigPresets(decryptedConfig, config, config.ignorePresets); let resolvedConfig = await decryptConfig(configToDecrypt, repository); logger.trace({ config: resolvedConfig }, "resolved config"); const migrationResult = migrateConfig(resolvedConfig); if (migrationResult.isMigrated) { logger.debug("Resolved config needs migrating"); logger.trace({ config: resolvedConfig }, "resolved config after migrating"); resolvedConfig = migrationResult.migratedConfig; } if (isString(resolvedConfig.npmrc)) logger.debug("Ignoring any .npmrc files in repository due to configured npmrc"); applyNpmrc(resolvedConfig, "resolved"); resolvedConfig = applySecretsAndVariablesToConfig({ config: resolvedConfig, secrets: mergeChildConfig(coerceObject(config.secrets), coerceObject(resolvedConfig.secrets)), variables: mergeChildConfig(coerceObject(config.variables), coerceObject(resolvedConfig.variables)) }); applyHostRules(resolvedConfig); returnConfig = mergeChildConfig(returnConfig, resolvedConfig); ({config: returnConfig} = await resolveConfigPresets(returnConfig, config)); returnConfig.renovateJsonPresent = true; // istanbul ignore if if (returnConfig.ignorePaths?.length) logger.debug({ ignorePaths: returnConfig.ignorePaths }, `Found repo ignorePaths`); setUserEnv(returnConfig.env); delete returnConfig.env; return returnConfig; } function applyNpmrc(config, configType) { setNpmTokenInNpmrc(config); if (!isString(config.npmrc)) return; logger.debug(`Setting npmrc from ${configType ? `${configType} ` : ""}config`); setNpmrc(config.npmrc); } function applyHostRules(config) { if (!config.hostRules) return; logger.debug("Setting hostRules from config"); for (const rule of config.hostRules) try { add(rule); } catch (err) { logger.warn({ err, config: rule }, "Error setting hostRule from config"); } clear(); clear$1(); delete config.hostRules; } /** needed when using portal secrets for npmToken */ function setNpmTokenInNpmrc(config) { if (!isString(config.npmToken)) return; const token = config.npmToken; logger.debug({ npmToken: maskToken(token) }, "Migrating npmToken to npmrc"); if (!isString(config.npmrc)) { logger.debug("Adding npmrc to config"); config.npmrc = `//registry.npmjs.org/:_authToken=${token}\n`; delete config.npmToken; return; } if (config.npmrc.includes(`\${NPM_TOKEN}`)) { logger.debug(`Replacing \${NPM_TOKEN} with npmToken`); config.npmrc = config.npmrc.replace(regEx(/\${NPM_TOKEN}/g), token); } else { logger.debug("Appending _authToken= to end of existing npmrc"); config.npmrc = config.npmrc.replace(regEx(/\n?$/), `\n_authToken=${token}\n`); } delete config.npmToken; } async function resolveStaticRepoConfig(config, filename) { if (!isNonEmptyString(filename)) return config; let staticRepoConfig; try { staticRepoConfig = await tryReadStaticRepoFileConfig(filename); } catch (err) { logger.fatal({ err }, "Failed to load static repository config file"); process.exit(1); } if (!isNonEmptyObject(staticRepoConfig)) return config; return mergeStaticConfig(config, staticRepoConfig); } async function tryReadStaticRepoFileConfig(staticRepoConfigFile) { logger.debug(`Reading static repo config file from ${staticRepoConfigFile}`); let staticRepoConfigRaw; try { staticRepoConfigRaw = await readSystemFile(staticRepoConfigFile, "utf8"); } catch (err) { throw new Error(`Failed to read static repo config file: "${staticRepoConfigFile}"`, { cause: err }); } const staticRepoConfig = parseJson(staticRepoConfigRaw, staticRepoConfigFile); const { errors, warnings } = await validateConfig("repo", staticRepoConfig); if (isNonEmptyArray(errors) || isNonEmptyArray(warnings)) logger.info({ errors, warnings }, "Static repo config validation issues detected"); else logger.debug({ staticRepoConfig }, "Static repository config file successfully parsed and validated"); return staticRepoConfig; } function mergeStaticConfig(config, staticRepoConfig) { if (isNonEmptyArray(staticRepoConfig.extends)) { config.extends = [...staticRepoConfig.extends, ...coerceArray(config.extends)]; delete staticRepoConfig.extends; } return mergeChildConfig(staticRepoConfig, config); } /** * Resolve everything but internal Renovate presets and log it out. * * This allows users to understand the fully resolved configuration, including any `github>`, `local>`, etc presets, but excluding anything that's internal to Renovate (which can be verbose and/or less relevant), and provides useful output for debugging purposes. * This is also known as the "shallow" config. * Due to caching, this doesn't add any additional requests. */ async function logShallowConfig(_decryptedConfig, _config) { const decryptedConfig = clone(_decryptedConfig); const config = clone(_config); const { config: resolvedConfig, visitedPresets } = await resolveConfigPresets(clone(decryptedConfig), clone(config), [], [], false); logger.debug({ renovateVersion: pkg.version, config: resolvedConfig, visitedPresets }, "Resolved shallow config, without merging internal presets"); } //#endregion export { applyHostRules, detectConfigFile, detectRepoFileConfig, mergeRenovateConfig }; //# sourceMappingURL=merge.js.map