UNPKG

renovate

Version:

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

417 lines (416 loc) • 17.1 kB
import { WORKER_FILE_UPDATE_FAILED } from "../../../../constants/error-messages.js"; import { coerceString } from "../../../../util/string.js"; import { logger } from "../../../../logger/index.js"; import { getFile } from "../../../../util/git/index.js"; import { extractPackageFile, get } from "../../../../modules/manager/index.js"; import { doAutoReplace } from "./auto-replace.js"; import { isNonEmptyArray } from "@sindresorhus/is"; //#region lib/workers/repository/update/branch/get-updated.ts async function getFileContent(updatedFileContents, filePath, config) { let fileContent = updatedFileContents[filePath]; if (!fileContent) fileContent = await getFile(filePath, config.reuseExistingBranch ? config.branchName : config.baseBranch); return fileContent; } function sortPackageFiles(config, manager, packageFiles) { const managerPackageFiles = config.packageFiles?.[manager]; if (!managerPackageFiles) return; packageFiles.sort((lhs, rhs) => { return managerPackageFiles.findIndex((entry) => entry.packageFile === lhs.path) - managerPackageFiles.findIndex((entry) => entry.packageFile === rhs.path); }); } function hasAny(set, targets) { for (const target of targets) if (set.has(target)) return true; return false; } function getManagersForPackageFiles(packageFiles, managerPackageFiles) { const packageFileNames = packageFiles.map((packageFile) => packageFile.path); return new Set(Object.keys(managerPackageFiles).filter((manager) => hasAny(managerPackageFiles[manager], packageFileNames))); } function getPackageFilesForManager(packageFiles, managerPackageFiles) { return packageFiles.filter((packageFile) => managerPackageFiles.has(packageFile.path)); } async function getUpdatedPackageFiles(config) { logger.trace({ config }); const reuseExistingBranch = config.reuseExistingBranch; logger.debug(`manager.getUpdatedPackageFiles() reuseExistingBranch=${reuseExistingBranch}`); let updatedFileContents = {}; const nonUpdatedFileContents = {}; const managerPackageFiles = {}; const packageFileUpdatedDeps = {}; const lockFileMaintenanceFiles = []; let firstUpdate = true; for (const upgrade of config.upgrades) { const manager = upgrade.manager; const packageFile = upgrade.packageFile; const depName = upgrade.depName; const newVersion = upgrade.newVersion; const currentVersion = upgrade.currentVersion; const updateLockedDependency = get(manager, "updateLockedDependency"); managerPackageFiles[manager] ??= /* @__PURE__ */ new Set(); managerPackageFiles[manager].add(packageFile); packageFileUpdatedDeps[packageFile] ??= []; packageFileUpdatedDeps[packageFile].push({ ...upgrade }); const packageFileContent = await getFileContent(updatedFileContents, packageFile, config); let lockFileContent = null; const lockFile = upgrade.lockFile ?? upgrade.lockFiles?.[0] ?? ""; if (lockFile) lockFileContent = await getFileContent(updatedFileContents, lockFile, config); // istanbul ignore if if (reuseExistingBranch && (!packageFileContent || lockFile && !lockFileContent)) { logger.debug({ packageFile, depName }, "Rebasing branch after file not found"); return getUpdatedPackageFiles({ ...config, reuseExistingBranch: false }); } if (upgrade.updateType === "lockFileMaintenance") lockFileMaintenanceFiles.push(packageFile); else if (upgrade.isRemediation) { const { status, files } = await updateLockedDependency({ ...upgrade, depName, newVersion, currentVersion, packageFile, packageFileContent, lockFile, lockFileContent, allowParentUpdates: true, allowHigherOrRemoved: true }); if (reuseExistingBranch && status !== "already-updated") { logger.debug({ lockFile, depName, status }, "Need to retry branch as it is not already up-to-date"); return getUpdatedPackageFiles({ ...config, reuseExistingBranch: false }); } if (files) { updatedFileContents = { ...updatedFileContents, ...files }; Object.keys(files).forEach((file) => delete nonUpdatedFileContents[file]); } if (status === "update-failed" || status === "unsupported") upgrade.remediationNotPossible = true; } else if (upgrade.isLockfileUpdate) if (updateLockedDependency) { const { status, files } = await updateLockedDependency({ ...upgrade, depName, newVersion, currentVersion, packageFile, packageFileContent, lockFile, lockFileContent, allowParentUpdates: false }); if (status === "unsupported") { if (!updatedFileContents[packageFile]) nonUpdatedFileContents[packageFile] = packageFileContent; } else if (status === "already-updated") logger.debug(`Upgrade of ${depName} to ${newVersion} is already done in existing branch`); else { if (reuseExistingBranch) { logger.debug({ lockFile, depName, status }, "Need to retry branch as upgrade requirements are not mets"); return getUpdatedPackageFiles({ ...config, reuseExistingBranch: false }); } if (files) { updatedFileContents = { ...updatedFileContents, ...files }; Object.keys(files).forEach((file) => delete nonUpdatedFileContents[file]); } } } else { logger.debug({ manager }, "isLockFileUpdate without updateLockedDependency"); if (!updatedFileContents[packageFile]) nonUpdatedFileContents[packageFile] = packageFileContent; } else { const updateDependency = get(manager, "updateDependency"); if (!updateDependency) { let res = await doAutoReplace(upgrade, packageFileContent, reuseExistingBranch, firstUpdate); firstUpdate = false; if (res) { res = await applyManagerBumpPackageVersion(res, upgrade); if (res === packageFileContent) logger.debug({ packageFile, depName }, "No content changed"); else { logger.debug({ packageFile, depName }, "Contents updated"); updatedFileContents[packageFile] = res; delete nonUpdatedFileContents[packageFile]; } continue; } else if (reuseExistingBranch) return getUpdatedPackageFiles({ ...config, reuseExistingBranch: false }); logger.error({ packageFile, depName }, "Could not autoReplace"); throw new Error(WORKER_FILE_UPDATE_FAILED); } let newContent = await updateDependency({ packageFile, fileContent: packageFileContent, upgrade }); newContent = await applyManagerBumpPackageVersion(newContent, upgrade); if (!newContent) { if (reuseExistingBranch) { logger.debug({ packageFile, depName }, "Rebasing branch after error updating content"); return getUpdatedPackageFiles({ ...config, reuseExistingBranch: false }); } logger.debug({ existingContent: packageFileContent, config: upgrade }, "Error updating file"); throw new Error(WORKER_FILE_UPDATE_FAILED); } if (newContent !== packageFileContent) { if (reuseExistingBranch) { logger.debug({ packageFile, depName }, "Need to update package file so will rebase first"); return getUpdatedPackageFiles({ ...config, reuseExistingBranch: false }); } logger.debug(`Updating ${depName} in ${coerceString(packageFile, lockFile)}`); updatedFileContents[packageFile] = newContent; delete nonUpdatedFileContents[packageFile]; } if (newContent === packageFileContent) { if (upgrade.manager === "git-submodules") { updatedFileContents[packageFile] = newContent; delete nonUpdatedFileContents[packageFile]; } } } } const updatedPackageFiles = Object.keys(updatedFileContents).map((name) => ({ type: "addition", path: name, contents: updatedFileContents[name] })); const updatedArtifacts = []; const artifactErrors = []; const artifactNotices = []; if (isNonEmptyArray(updatedPackageFiles)) { logger.debug("updateArtifacts for updatedPackageFiles"); const updatedPackageFileManagers = getManagersForPackageFiles(updatedPackageFiles, managerPackageFiles); for (const manager of updatedPackageFileManagers) { const packageFilesForManager = getPackageFilesForManager(updatedPackageFiles, managerPackageFiles[manager]); sortPackageFiles(config, manager, packageFilesForManager); for (const packageFile of packageFilesForManager) { const updatedDeps = packageFileUpdatedDeps[packageFile.path]; const results = await managerUpdateArtifacts(manager, { packageFileName: packageFile.path, updatedDeps, newPackageFileContent: packageFile.contents.toString(), config: patchConfigForArtifactsUpdate(config, manager, packageFile.path) }); processUpdateArtifactResults(results, updatedArtifacts, artifactErrors, artifactNotices); if (isNonEmptyArray(results)) await checkForPendingVersions(manager, packageFile.path, packageFile.contents.toString(), updatedDeps, artifactErrors, config); } } } const nonUpdatedPackageFiles = Object.keys(nonUpdatedFileContents).map((name) => ({ type: "addition", path: name, contents: nonUpdatedFileContents[name] })); if (isNonEmptyArray(nonUpdatedPackageFiles)) { logger.debug("updateArtifacts for nonUpdatedPackageFiles"); const nonUpdatedPackageFileManagers = getManagersForPackageFiles(nonUpdatedPackageFiles, managerPackageFiles); for (const manager of nonUpdatedPackageFileManagers) { const packageFilesForManager = getPackageFilesForManager(nonUpdatedPackageFiles, managerPackageFiles[manager]); sortPackageFiles(config, manager, packageFilesForManager); for (const packageFile of packageFilesForManager) { const updatedDeps = packageFileUpdatedDeps[packageFile.path]; const results = await managerUpdateArtifacts(manager, { packageFileName: packageFile.path, updatedDeps, newPackageFileContent: packageFile.contents.toString(), config: patchConfigForArtifactsUpdate(config, manager, packageFile.path) }); processUpdateArtifactResults(results, updatedArtifacts, artifactErrors, artifactNotices); if (isNonEmptyArray(results)) { updatedPackageFiles.push(packageFile); await checkForPendingVersions(manager, packageFile.path, packageFile.contents.toString(), updatedDeps, artifactErrors, config); } } } } if (!reuseExistingBranch) { const lockFileMaintenancePackageFiles = lockFileMaintenanceFiles.map((name) => ({ path: name })); if (isNonEmptyArray(lockFileMaintenanceFiles)) { logger.debug("updateArtifacts for lockFileMaintenanceFiles"); const lockFileMaintenanceManagers = getManagersForPackageFiles(lockFileMaintenancePackageFiles, managerPackageFiles); for (const manager of lockFileMaintenanceManagers) { const packageFilesForManager = getPackageFilesForManager(lockFileMaintenancePackageFiles, managerPackageFiles[manager]); sortPackageFiles(config, manager, packageFilesForManager); for (const packageFile of packageFilesForManager) { const contents = updatedFileContents[packageFile.path] || await getFile(packageFile.path, config.baseBranch); processUpdateArtifactResults(await managerUpdateArtifacts(manager, { packageFileName: packageFile.path, updatedDeps: [], newPackageFileContent: contents, config: patchConfigForArtifactsUpdate(config, manager, packageFile.path) }), updatedArtifacts, artifactErrors, artifactNotices); } } } } return { reuseExistingBranch, updatedPackageFiles, updatedArtifacts, artifactErrors, artifactNotices }; } function patchConfigForArtifactsUpdate(config, manager, packageFileName) { const updatedConfig = { ...config }; delete updatedConfig.lockFiles; if (isNonEmptyArray(updatedConfig.packageFiles?.[manager])) { const packageFile = (updatedConfig.packageFiles?.[manager]).find((p) => p.packageFile === packageFileName); if (packageFile && isNonEmptyArray(packageFile.lockFiles)) updatedConfig.lockFiles = packageFile.lockFiles; } return updatedConfig; } async function managerUpdateArtifacts(manager, updateArtifact) { const updateArtifacts = get(manager, "updateArtifacts"); if (!updateArtifacts) return null; if (updateArtifact.config.skipArtifactsUpdate) { logger.debug({ manager, packageFileName: updateArtifact.packageFileName }, "Skipping artifact update"); return null; } return await updateArtifacts(updateArtifact); } function processUpdateArtifactResults(results, updatedArtifacts, artifactErrors, artifactNotices) { if (isNonEmptyArray(results)) for (const res of results) { const { file, notice, artifactError } = res; if (file) updatedArtifacts.push(file); if (artifactError) artifactErrors.push(artifactError); if (notice) artifactNotices.push(notice); } } /** * When using Minimum Release Age, and a package manager that doesn't support being told an explicit version to update to (#41624) it is possible that an artifact update leads to a different version of a dependency being used compared to what Renovate is expecting. * * We should report these cases more explicitly with an Artifact Error, to allow the reviewers to decide what to do with the changes. */ async function checkForPendingVersions(manager, packageFileName, packageFileContent, updatedDeps, artifactErrors, config) { const depNameToUpgradeInfo = /* @__PURE__ */ new Map(); for (const dep of updatedDeps) if (dep.depName && isNonEmptyArray(dep.pendingVersions)) depNameToUpgradeInfo.set(dep.depName, { pendingVersions: new Set(dep.pendingVersions), newVersion: dep.newVersion }); if (!depNameToUpgradeInfo.size) return; const extracted = await extractPackageFile(manager, packageFileContent, packageFileName, config); if (!extracted) { logger.warn({ packageFile: packageFileName, manager }, "Could not re-extract the packageFile after updating it"); return; } for (const dep of extracted.deps) { const depName = dep.depName ?? dep.packageName; if (!depName) { logger.error({ packageFile: packageFileName, manager, branchName: config.branchName, depName: dep.depName }, `No depName found after updating '${packageFileName}'`); throw new Error(WORKER_FILE_UPDATE_FAILED); } const upgradeInfo = depNameToUpgradeInfo.get(depName); if (!upgradeInfo) continue; const resolvedVersion = dep.lockedVersion ?? dep.newVersion ?? dep.currentVersion ?? dep.currentValue; if (!resolvedVersion) { logger.warn({ packageFile: packageFileName, manager, branchName: config.branchName, depName }, `Could not determine resolved version for '${depName}' after updating '${packageFileName}'; skipping pending-version check`); continue; } if (resolvedVersion && upgradeInfo.pendingVersions.has(resolvedVersion)) { const expectedVersion = upgradeInfo.newVersion; /* v8 ignore next if -- should not happen */ if (!expectedVersion) { logger.error({ packageFile: packageFileName, manager, branchName: config.branchName, depName, newVersion: resolvedVersion, expectedVersion }, `No expectedVersion found for '${depName}' after updating '${packageFileName}'`); continue; } if (config.minimumReleaseAgeBehaviour === "timestamp-optional") { logger.once.warn({ packageFileName, depName, expectedVersion, resolvedVersion }, "Artifact error would be reported due to a pending version in use which hasn't passed Minimum Release Age, but as we're running with minimumReleaseAgeBehaviour=timestamp-optional, proceeding. See debug logs for more information"); continue; } logger.debug({ packageFileName, depName, expectedVersion, resolvedVersion }, "Artifact update introduced a pending version"); let stderr = `Artifact update for ${depName} resolved to version ${resolvedVersion}, which is a pending version that has not yet passed the Minimum Release Age threshold.`; stderr += `\nRenovate was attempting to update to ${expectedVersion}`; stderr += `\nThis is (likely) not a bug in Renovate, but due to the way your project pins dependencies, _and_ how Renovate calls your package manager to update them.\nUntil Renovate supports specifying an exact update to your package manager (https://github.com/renovatebot/renovate/issues/41624), it is recommended to directly pin your dependencies (with \`rangeStrategy=pin\` for apps, or \`rangeStrategy=widen\` for libraries)\nSee also: https://docs.renovatebot.com/dependency-pinning/`; artifactErrors.push({ fileName: packageFileName, stderr }); } } } async function applyManagerBumpPackageVersion(packageFileContent, upgrade) { const bumpPackageVersion = get(upgrade.manager, "bumpPackageVersion"); if (!bumpPackageVersion || !packageFileContent || !upgrade.bumpVersion || !upgrade.packageFileVersion) return packageFileContent; return (await bumpPackageVersion(packageFileContent, upgrade.packageFileVersion, upgrade.bumpVersion, upgrade.packageFile)).bumpedContent; } //#endregion export { getUpdatedPackageFiles }; //# sourceMappingURL=get-updated.js.map