renovate
Version:
Automated dependency updates. Flexible so you don't need to be.
375 lines (374 loc) • 16 kB
JavaScript
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