UNPKG

renovate

Version:

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

375 lines (374 loc) • 16 kB
import "../../../../constants/error-messages.js"; import { pkg } from "../../../../expose.js"; import { GlobalConfig } from "../../../../config/global.js"; import { logger } from "../../../../logger/index.js"; import { ExternalHostError } from "../../../../types/errors/external-host-error.js"; import { incCountValue, isLimitReached } from "../../../global/limits.js"; import { getBranchLastCommitTime } from "../../../../util/git/index.js"; import { getElapsedHours } from "../../../../util/date.js"; import { stripEmojis } from "../../../../util/emoji.js"; import { getPrBodyStruct, hashBody } from "../../../../modules/platform/pr-body.js"; import { scm } from "../../../../modules/platform/scm.js"; import { platform } from "../../../../modules/platform/index.js"; import { getPrCache, setPrCache } from "./pr-cache.js"; import { ensureComment } from "../../../../modules/platform/comment.js"; import { fingerprint } from "../../../../util/fingerprint.js"; import { memoize } from "../../../../util/memoize.js"; import { embedChangelogs } from "../../changelog/index.js"; import { resolveBranchStatus } from "../branch/status-checks.js"; import { getPrBody } from "./body/index.js"; import { getChangedLabels, prepareLabels, shouldUpdateLabels } from "./labels.js"; import { addParticipants } from "./participants.js"; import { generatePrBodyFingerprintConfig, validatePrCache } from "./pr-fingerprint.js"; import { tryReuseAutoclosedPr } from "./pr-reuse.js"; import { isArray, isNonEmptyArray, isNumber } from "@sindresorhus/is"; //#region lib/workers/repository/update/pr/index.ts function getPlatformPrOptions(config) { const usePlatformAutomerge = Boolean(config.automerge && (config.automergeType === "pr" || config.automergeType === "branch") && config.platformAutomerge); return { autoApprove: !!config.autoApprove, automergeCommitMessage: config.commitMessage, automergeStrategy: config.automergeStrategy, azureWorkItemId: config.azureWorkItemId ?? 0, bbAutoResolvePrTasks: !!config.bbAutoResolvePrTasks, bbUseDefaultReviewers: !!config.bbUseDefaultReviewers, gitLabIgnoreApprovals: !!config.gitLabIgnoreApprovals, forkModeDisallowMaintainerEdits: !!config.forkModeDisallowMaintainerEdits, usePlatformAutomerge }; } function updatePrDebugData(targetBranch, labels, debugData) { const updatedPrDebugData = { createdInVer: debugData?.createdInVer ?? pkg.version, updatedInVer: pkg.version, targetBranch }; if (!debugData || isArray(debugData.labels)) updatedPrDebugData.labels = labels; return updatedPrDebugData; } function hasNotIgnoredReviewers(pr, config) { if (isNonEmptyArray(config.ignoreReviewers) && isNonEmptyArray(pr.reviewers)) { const ignoreReviewers = new Set(config.ignoreReviewers); return pr.reviewers.filter((reviewer) => !ignoreReviewers.has(reviewer)).length > 0; } return isNonEmptyArray(pr.reviewers); } function addPullRequestNoteIfAttestationHasBeenLost(upgrade, currentReleaseHasAttestation) { const { packageName, depName, currentVersion, newVersion } = upgrade; const name = packageName ?? depName; const newRelease = upgrade.releases?.find((release) => release.version === newVersion); if (newRelease && currentReleaseHasAttestation === true && newRelease.attestation !== true) { upgrade.prBodyNotes ??= []; upgrade.prBodyNotes.push([ "> :stop_sign: **Caution**", ">", `> ${name} ${currentVersion} was released with an attestation, but ${newVersion} has no attestation.`, `> Verify that release ${newVersion} was published by the expected author.`, "\n" ].join("\n")); } } async function ensurePr(prConfig) { const config = { ...prConfig }; const prBodyFingerprint = fingerprint(generatePrBodyFingerprintConfig(config)); logger.trace({ config }, "ensurePr"); const { branchName, ignoreTests, internalChecksAsSuccess, prTitle = "", upgrades, hasAttestation: currentReleaseHasAttestation } = config; const getBranchStatus = memoize(() => resolveBranchStatus(branchName, !!internalChecksAsSuccess, ignoreTests)); const dependencyDashboardCheck = config.dependencyDashboardChecks?.[config.branchName]; const existingPr = await platform.getBranchPr(branchName, config.baseBranch) ?? await tryReuseAutoclosedPr(branchName, prTitle); const prCache = getPrCache(branchName); if (existingPr) { logger.debug("Found existing PR"); if (existingPr.bodyStruct?.rebaseRequested) logger.debug("PR rebase requested, so skipping cache check"); else if (prCache) { logger.trace({ prCache }, "Found existing PR cache"); if (validatePrCache(prCache, prBodyFingerprint) && !config.autoApprove) return { type: "with-pr", pr: existingPr }; } else if (config.repositoryCache === "enabled") logger.debug("PR cache not found"); } config.upgrades = []; if (config.artifactErrors?.length) { logger.debug("Forcing PR because of artifact errors"); config.forcePr = true; } if (dependencyDashboardCheck === "approvePr") { logger.debug("Forcing PR because of dependency dashboard approval"); config.forcePr = true; } if (!existingPr) { if (config.automerge === true && config.automergeType?.startsWith("branch") && !config.forcePr) { logger.debug(`Branch automerge is enabled`); if (config.stabilityStatus !== "yellow" && await getBranchStatus() === "yellow" && isNumber(config.prNotPendingHours)) { logger.debug("Checking how long this branch has been pending"); if (getElapsedHours(await getBranchLastCommitTime(branchName)) >= config.prNotPendingHours) { logger.debug(`Branch exceeds prNotPending=${config.prNotPendingHours}, hours - forcing PR creation`); config.forcePr = true; } } if (config.forcePr || await getBranchStatus() === "red") logger.debug(`Branch tests failed, so will create PR`); else return { type: "without-pr", prBlockedBy: "BranchAutomerge" }; } if (config.prCreation === "status-success") { logger.debug("Checking branch combined status"); if (await getBranchStatus() !== "green") { logger.debug(`Branch status isn't green - not creating PR`); return { type: "without-pr", prBlockedBy: "AwaitingTests" }; } logger.debug("Branch status success"); } else if (config.prCreation === "approval" && dependencyDashboardCheck !== "approvePr") return { type: "without-pr", prBlockedBy: "NeedsApproval" }; else if (config.prCreation === "not-pending" && !config.forcePr) { logger.debug("Checking branch combined status"); if (await getBranchStatus() === "yellow") { logger.debug(`Branch status is yellow - checking timeout`); const elapsedHours = getElapsedHours(await getBranchLastCommitTime(branchName)); if (!dependencyDashboardCheck && (config.stabilityStatus && config.stabilityStatus !== "yellow" || isNumber(config.prNotPendingHours) && elapsedHours < config.prNotPendingHours)) { logger.debug(`Branch is ${elapsedHours} hours old - skipping PR creation as prNotPendingHours is set to ${config.prNotPendingHours}`); return { type: "without-pr", prBlockedBy: "AwaitingTests" }; } const prNotPendingHours = String(config.prNotPendingHours); logger.debug(`prNotPendingHours=${prNotPendingHours} threshold hit - creating PR`); } logger.debug("Branch status success"); } } const processedUpgrades = []; const commitRepos = []; function getRepoNameWithSourceDirectory(upgrade) { return `${upgrade.repoName}${upgrade.sourceDirectory ? `:${upgrade.sourceDirectory}` : ""}`; } await embedChangelogs({ upgrades, stage: "pr" }); for (const upgrade of upgrades) { const upgradeKey = `${upgrade.depType}-${upgrade.depName}-${upgrade.manager}-${upgrade.currentVersion ?? ""}-${upgrade.currentValue ?? ""}-${upgrade.newVersion ?? ""}-${upgrade.newValue ?? ""}`; if (processedUpgrades.includes(upgradeKey)) continue; processedUpgrades.push(upgradeKey); const logJSON = upgrade.logJSON; if (logJSON) { if (typeof logJSON.error === "undefined") { if (logJSON.project) upgrade.repoName = logJSON.project.repository; upgrade.hasReleaseNotes = false; upgrade.releases = []; if (logJSON.hasReleaseNotes && upgrade.repoName && !commitRepos.includes(getRepoNameWithSourceDirectory(upgrade))) { commitRepos.push(getRepoNameWithSourceDirectory(upgrade)); upgrade.hasReleaseNotes = logJSON.hasReleaseNotes; if (logJSON.versions) for (const version of logJSON.versions) { const release = { ...version }; upgrade.releases.push(release); } } } else if (logJSON.error === "MissingGithubToken") { upgrade.prBodyNotes ??= []; upgrade.prBodyNotes.push([ "> :exclamation: **Important**", "> ", "> Release Notes retrieval for this PR were skipped because no github.com credentials were available. ", "> If you are self-hosted, please see [this instruction](https://github.com/renovatebot/renovate/blob/master/docs/usage/examples/self-hosting.md#githubcom-token-for-release-notes).", "\n" ].join("\n")); } } addPullRequestNoteIfAttestationHasBeenLost(upgrade, currentReleaseHasAttestation); config.upgrades.push(upgrade); } config.hasReleaseNotes = config.upgrades.some((upg) => upg.hasReleaseNotes); const releaseNotesSources = []; for (const upgrade of config.upgrades) { let notesSourceUrl = upgrade.releases?.[0]?.releaseNotes?.notesSourceUrl; notesSourceUrl ??= `${upgrade.sourceUrl}${upgrade.sourceDirectory ? `:${upgrade.sourceDirectory}` : ""}`; if (upgrade.hasReleaseNotes && notesSourceUrl) if (releaseNotesSources.includes(notesSourceUrl)) { logger.debug({ depName: upgrade.depName }, "Removing duplicate release notes"); upgrade.hasReleaseNotes = false; } else releaseNotesSources.push(notesSourceUrl); } const prBody = getPrBody(config, { debugData: updatePrDebugData(config.baseBranch, prepareLabels(config), existingPr?.bodyStruct?.debugData) }, config); try { if (existingPr) { logger.debug("Processing existing PR"); if (!existingPr.hasAssignees && !hasNotIgnoredReviewers(existingPr, config) && config.automerge && !config.assignAutomerge && await getBranchStatus() === "red") { logger.debug(`Setting assignees and reviewers as status checks failed`); await addParticipants(config, existingPr); } const existingPrTitle = stripEmojis(existingPr.title); const existingPrBodyHash = existingPr.bodyStruct?.hash; const newPrTitle = stripEmojis(prTitle); const newPrBodyHash = hashBody(prBody); const prInitialLabels = existingPr.bodyStruct?.debugData?.labels; const prCurrentLabels = existingPr.labels; const configuredLabels = prepareLabels(config); const labelsNeedUpdate = shouldUpdateLabels(prInitialLabels, prCurrentLabels, configuredLabels); if (existingPr?.targetBranch === config.baseBranch && existingPrTitle === newPrTitle && existingPrBodyHash === newPrBodyHash && !labelsNeedUpdate && !config.autoApprove) { setPrCache(branchName, prBodyFingerprint, false); logger.debug(`Pull Request #${existingPr.number} does not need updating`); return { type: "with-pr", pr: existingPr }; } const updatePrConfig = { number: existingPr.number, prTitle, prBody, platformPrOptions: getPlatformPrOptions(config) }; if (existingPr?.targetBranch !== config.baseBranch) { logger.debug({ branchName, oldBaseBranch: existingPr?.targetBranch, newBaseBranch: config.baseBranch }, "PR base branch has changed"); updatePrConfig.targetBranch = config.baseBranch; } if (labelsNeedUpdate) { logger.debug({ branchName, prCurrentLabels, configuredLabels }, "PR labels have changed"); const [addLabels, removeLabels] = getChangedLabels(prCurrentLabels, configuredLabels); updatePrConfig.labels = configuredLabels; updatePrConfig.addLabels = addLabels; updatePrConfig.removeLabels = removeLabels; } if (existingPrTitle !== newPrTitle) logger.debug({ branchName, oldPrTitle: existingPr.title, newPrTitle: prTitle }, "PR title changed"); else if (!config.committedFiles && !config.rebaseRequested) logger.debug({ prTitle }, "PR body changed"); if (GlobalConfig.get("dryRun")) { logger.info(`DRY-RUN: Would update PR #${existingPr.number}`); return { type: "with-pr", pr: existingPr }; } else { await platform.updatePr(updatePrConfig); logger.info({ pr: existingPr.number, prTitle }, `PR updated`); setPrCache(branchName, prBodyFingerprint, true); } return { type: "with-pr", pr: { ...existingPr, bodyStruct: getPrBodyStruct(prBody), title: prTitle, targetBranch: config.baseBranch } }; } logger.debug({ branch: branchName, prTitle }, `Creating PR`); if (config.updateType === "rollback") logger.info("Creating Rollback PR"); let pr; if (GlobalConfig.get("dryRun")) { logger.info({ labels: prepareLabels(config) }, `DRY-RUN: Would create PR: ${prTitle}`); pr = { number: 0 }; } else try { if (!dependencyDashboardCheck && isLimitReached("ConcurrentPRs", prConfig) && !config.isVulnerabilityAlert) { logger.debug("Skipping PR - limit reached"); return { type: "without-pr", prBlockedBy: "RateLimited" }; } pr = await platform.createPr({ sourceBranch: branchName, targetBranch: config.baseBranch, prTitle, prBody, labels: prepareLabels(config), platformPrOptions: getPlatformPrOptions(config), draftPR: !!config.draftPR, milestone: config.milestone }); incCountValue("ConcurrentPRs"); incCountValue("HourlyPRs"); logger.info({ pr: pr?.number, prTitle, labels: pr?.labels }, "PR created"); } catch (err) { logger.debug({ err }, "Pull request creation error"); if (err.body?.message === "Validation failed" && err.body.errors?.length && err.body.errors.some((error) => error.message?.startsWith("A pull request already exists"))) { logger.warn("A pull requests already exists"); return { type: "without-pr", prBlockedBy: "Error" }; } if (err.statusCode === 502) { logger.warn({ branch: branchName }, "Deleting branch due to server error"); await scm.deleteBranch(branchName); } return { type: "without-pr", prBlockedBy: "Error" }; } if (pr && config.branchAutomergeFailureMessage && !config.suppressNotifications?.includes("branchAutomergeFailure")) { const topic = "Branch automerge failure"; let content = "This PR was configured for branch automerge. However, this is not possible, so it has been raised as a PR instead."; if (config.branchAutomergeFailureMessage === "branch status error") content += "\n___\n * Branch has one or more failed status checks"; content = platform.massageMarkdown(content, config.rebaseLabel); logger.debug("Adding branch automerge failure message to PR"); if (GlobalConfig.get("dryRun")) logger.info(`DRY-RUN: Would add comment to PR #${pr.number}`); else await ensureComment({ number: pr.number, topic, content }); } if (pr) { if (config.automerge && !config.assignAutomerge && await getBranchStatus() !== "red") logger.debug(`Skipping assignees and reviewers as automerge=${config.automerge}`); else await addParticipants(config, pr); setPrCache(branchName, prBodyFingerprint, true); logger.debug(`Created Pull Request #${pr.number}`); return { type: "with-pr", pr }; } } catch (err) { if (err instanceof ExternalHostError || err.message === "repository-changed" || err.message === "rate-limit-exceeded" || err.message === "integration-unauthorized") { logger.debug("Passing error up"); throw err; } logger.warn({ err, prTitle }, "Failed to ensure PR"); } if (existingPr) return { type: "with-pr", pr: existingPr }; return { type: "without-pr", prBlockedBy: "Error" }; } //#endregion export { ensurePr, getPlatformPrOptions }; //# sourceMappingURL=index.js.map