renovate
Version:
Automated dependency updates. Flexible so you don't need to be.
435 lines • 21.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.getPlatformPrOptions = getPlatformPrOptions;
exports.updatePrDebugData = updatePrDebugData;
exports.ensurePr = ensurePr;
const tslib_1 = require("tslib");
const is_1 = tslib_1.__importDefault(require("@sindresorhus/is"));
const global_1 = require("../../../../config/global");
const error_messages_1 = require("../../../../constants/error-messages");
const expose_cjs_1 = require("../../../../expose.cjs");
const logger_1 = require("../../../../logger");
const platform_1 = require("../../../../modules/platform");
const comment_1 = require("../../../../modules/platform/comment");
const pr_body_1 = require("../../../../modules/platform/pr-body");
const scm_1 = require("../../../../modules/platform/scm");
const external_host_error_1 = require("../../../../types/errors/external-host-error");
const date_1 = require("../../../../util/date");
const emoji_1 = require("../../../../util/emoji");
const fingerprint_1 = require("../../../../util/fingerprint");
const git_1 = require("../../../../util/git");
const memoize_1 = require("../../../../util/memoize");
const limits_1 = require("../../../global/limits");
const changelog_1 = require("../../changelog");
const status_checks_1 = require("../branch/status-checks");
const body_1 = require("./body");
const labels_1 = require("./labels");
const participants_1 = require("./participants");
const pr_cache_1 = require("./pr-cache");
const pr_fingerprint_1 = require("./pr-fingerprint");
const pr_reuse_1 = require("./pr-reuse");
function getPlatformPrOptions(config) {
const usePlatformAutomerge = Boolean(config.automerge &&
(config.automergeType === 'pr' || config.automergeType === 'branch') &&
config.platformAutomerge);
return {
autoApprove: !!config.autoApprove,
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 createdByRenovateVersion = debugData?.createdInVer ?? expose_cjs_1.pkg.version;
const updatedByRenovateVersion = expose_cjs_1.pkg.version;
const updatedPrDebugData = {
createdInVer: createdByRenovateVersion,
updatedInVer: updatedByRenovateVersion,
targetBranch,
};
// Add labels to the debug data object.
// When to add:
// 1. Add it when a new PR is created, i.e., when debugData is undefined.
// 2. Add it if an existing PR already has labels in the debug data, confirming that we can update its labels.
if (!debugData || is_1.default.array(debugData.labels)) {
updatedPrDebugData.labels = labels;
}
return updatedPrDebugData;
}
function hasNotIgnoredReviewers(pr, config) {
if (is_1.default.nonEmptyArray(config.ignoreReviewers) &&
is_1.default.nonEmptyArray(pr.reviewers)) {
const ignoreReviewers = new Set(config.ignoreReviewers);
return (pr.reviewers.filter((reviewer) => !ignoreReviewers.has(reviewer)).length >
0);
}
return is_1.default.nonEmptyArray(pr.reviewers);
}
// Ensures that PR exists with matching title/body
async function ensurePr(prConfig) {
const config = { ...prConfig };
const filteredPrConfig = (0, pr_fingerprint_1.generatePrBodyFingerprintConfig)(config);
const prBodyFingerprint = (0, fingerprint_1.fingerprint)(filteredPrConfig);
logger_1.logger.trace({ config }, 'ensurePr');
// If there is a group, it will use the config of the first upgrade in the array
const { branchName, ignoreTests, internalChecksAsSuccess, prTitle = '', upgrades, } = config;
const getBranchStatus = (0, memoize_1.memoize)(() => (0, status_checks_1.resolveBranchStatus)(branchName, !!internalChecksAsSuccess, ignoreTests));
const dependencyDashboardCheck = config.dependencyDashboardChecks?.[config.branchName];
// Check if PR already exists
const existingPr = (await platform_1.platform.getBranchPr(branchName, config.baseBranch)) ??
(await (0, pr_reuse_1.tryReuseAutoclosedPr)(branchName));
const prCache = (0, pr_cache_1.getPrCache)(branchName);
if (existingPr) {
logger_1.logger.debug('Found existing PR');
if (existingPr.bodyStruct?.rebaseRequested) {
logger_1.logger.debug('PR rebase requested, so skipping cache check');
}
else if (prCache) {
logger_1.logger.trace({ prCache }, 'Found existing PR cache');
// return if pr cache is valid and pr was not changed in the past 24hrs
if ((0, pr_fingerprint_1.validatePrCache)(prCache, prBodyFingerprint)) {
return { type: 'with-pr', pr: existingPr };
}
}
else if (config.repositoryCache === 'enabled') {
logger_1.logger.debug('PR cache not found');
}
}
config.upgrades = [];
if (config.artifactErrors?.length) {
logger_1.logger.debug('Forcing PR because of artifact errors');
config.forcePr = true;
}
if (dependencyDashboardCheck === 'approvePr') {
logger_1.logger.debug('Forcing PR because of dependency dashboard approval');
config.forcePr = true;
}
if (!existingPr) {
// Only create a PR if a branch automerge has failed
if (config.automerge === true &&
config.automergeType?.startsWith('branch') &&
!config.forcePr) {
logger_1.logger.debug(`Branch automerge is enabled`);
if (config.stabilityStatus !== 'yellow' &&
(await getBranchStatus()) === 'yellow' &&
is_1.default.number(config.prNotPendingHours)) {
logger_1.logger.debug('Checking how long this branch has been pending');
const lastCommitTime = await (0, git_1.getBranchLastCommitTime)(branchName);
if ((0, date_1.getElapsedHours)(lastCommitTime) >= config.prNotPendingHours) {
logger_1.logger.debug('Branch exceeds prNotPending hours - forcing PR creation');
config.forcePr = true;
}
}
if (config.forcePr || (await getBranchStatus()) === 'red') {
logger_1.logger.debug(`Branch tests failed, so will create PR`);
}
else {
// Branch should be automerged, so we don't want to create a PR
return { type: 'without-pr', prBlockedBy: 'BranchAutomerge' };
}
}
if (config.prCreation === 'status-success') {
logger_1.logger.debug('Checking branch combined status');
if ((await getBranchStatus()) !== 'green') {
logger_1.logger.debug(`Branch status isn't green - not creating PR`);
return { type: 'without-pr', prBlockedBy: 'AwaitingTests' };
}
logger_1.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_1.logger.debug('Checking branch combined status');
if ((await getBranchStatus()) === 'yellow') {
logger_1.logger.debug(`Branch status is yellow - checking timeout`);
const lastCommitTime = await (0, git_1.getBranchLastCommitTime)(branchName);
const elapsedHours = (0, date_1.getElapsedHours)(lastCommitTime);
if (!dependencyDashboardCheck &&
((config.stabilityStatus && config.stabilityStatus !== 'yellow') ||
(is_1.default.number(config.prNotPendingHours) &&
elapsedHours < config.prNotPendingHours))) {
logger_1.logger.debug(`Branch is ${elapsedHours} hours old - skipping PR creation`);
return {
type: 'without-pr',
prBlockedBy: 'AwaitingTests',
};
}
const prNotPendingHours = String(config.prNotPendingHours);
logger_1.logger.debug(`prNotPendingHours=${prNotPendingHours} threshold hit - creating PR`);
}
logger_1.logger.debug('Branch status success');
}
}
const processedUpgrades = [];
const commitRepos = [];
function getRepoNameWithSourceDirectory(upgrade) {
// TODO: types (#22198)
return `${upgrade.repoName}${upgrade.sourceDirectory ? `:${upgrade.sourceDirectory}` : ''}`;
}
if (config.fetchChangeLogs === 'pr') {
// fetch changelogs when not already done;
await (0, changelog_1.embedChangelogs)(upgrades);
}
// Get changelog and then generate template strings
for (const upgrade of upgrades) {
// TODO: types (#22198)
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 = [
...upgrade.prBodyNotes,
[
'> :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'),
];
}
}
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;
// TODO: types (#22198)
notesSourceUrl ??= `${upgrade.sourceUrl}${upgrade.sourceDirectory ? `:${upgrade.sourceDirectory}` : ''}`;
if (upgrade.hasReleaseNotes && notesSourceUrl) {
if (releaseNotesSources.includes(notesSourceUrl)) {
logger_1.logger.debug({ depName: upgrade.depName }, 'Removing duplicate release notes');
upgrade.hasReleaseNotes = false;
}
else {
releaseNotesSources.push(notesSourceUrl);
}
}
}
const prBody = (0, body_1.getPrBody)(config, {
debugData: updatePrDebugData(config.baseBranch, (0, labels_1.prepareLabels)(config), // include labels in debug data
existingPr?.bodyStruct?.debugData),
}, config);
try {
if (existingPr) {
logger_1.logger.debug('Processing existing PR');
if (!existingPr.hasAssignees &&
!hasNotIgnoredReviewers(existingPr, config) &&
config.automerge &&
!config.assignAutomerge &&
(await getBranchStatus()) === 'red') {
logger_1.logger.debug(`Setting assignees and reviewers as status checks failed`);
await (0, participants_1.addParticipants)(config, existingPr);
}
// Check if existing PR needs updating
const existingPrTitle = (0, emoji_1.stripEmojis)(existingPr.title);
const existingPrBodyHash = existingPr.bodyStruct?.hash;
const newPrTitle = (0, emoji_1.stripEmojis)(prTitle);
const newPrBodyHash = (0, pr_body_1.hashBody)(prBody);
const prInitialLabels = existingPr.bodyStruct?.debugData?.labels;
const prCurrentLabels = existingPr.labels;
const configuredLabels = (0, labels_1.prepareLabels)(config);
const labelsNeedUpdate = (0, labels_1.shouldUpdateLabels)(prInitialLabels, prCurrentLabels, configuredLabels);
if (existingPr?.targetBranch === config.baseBranch &&
existingPrTitle === newPrTitle &&
existingPrBodyHash === newPrBodyHash &&
!labelsNeedUpdate) {
// adds or-cache for existing PRs
(0, pr_cache_1.setPrCache)(branchName, prBodyFingerprint, false);
logger_1.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),
};
// PR must need updating
if (existingPr?.targetBranch !== config.baseBranch) {
logger_1.logger.debug({
branchName,
oldBaseBranch: existingPr?.targetBranch,
newBaseBranch: config.baseBranch,
}, 'PR base branch has changed');
updatePrConfig.targetBranch = config.baseBranch;
}
if (labelsNeedUpdate) {
logger_1.logger.debug({
branchName,
prCurrentLabels,
configuredLabels,
}, 'PR labels have changed');
// Divide labels into three categories:
// i) addLabels: Labels that need to be added
// ii) removeLabels: Labels that need to be removed
// iii) labels: New labels for the PR, replacing the old labels array entirely.
// This distinction is necessary because different platforms update labels differently
// For more details, refer to the updatePr function of each platform.
const [addLabels, removeLabels] = (0, labels_1.getChangedLabels)(prCurrentLabels, configuredLabels);
// for Gitea
updatePrConfig.labels = configuredLabels;
// for GitHub, GitLab
updatePrConfig.addLabels = addLabels;
updatePrConfig.removeLabels = removeLabels;
}
if (existingPrTitle !== newPrTitle) {
logger_1.logger.debug({
branchName,
oldPrTitle: existingPr.title,
newPrTitle: prTitle,
}, 'PR title changed');
}
else if (!config.committedFiles && !config.rebaseRequested) {
logger_1.logger.debug({
prTitle,
}, 'PR body changed');
}
if (global_1.GlobalConfig.get('dryRun')) {
logger_1.logger.info(`DRY-RUN: Would update PR #${existingPr.number}`);
return { type: 'with-pr', pr: existingPr };
}
else {
await platform_1.platform.updatePr(updatePrConfig);
logger_1.logger.info({ pr: existingPr.number, prTitle }, `PR updated`);
(0, pr_cache_1.setPrCache)(branchName, prBodyFingerprint, true);
}
return {
type: 'with-pr',
pr: {
...existingPr,
bodyStruct: (0, pr_body_1.getPrBodyStruct)(prBody),
title: prTitle,
targetBranch: config.baseBranch,
},
};
}
logger_1.logger.debug({ branch: branchName, prTitle }, `Creating PR`);
if (config.updateType === 'rollback') {
logger_1.logger.info('Creating Rollback PR');
}
let pr;
if (global_1.GlobalConfig.get('dryRun')) {
logger_1.logger.info('DRY-RUN: Would create PR: ' + prTitle);
pr = { number: 0 };
}
else {
try {
if (!dependencyDashboardCheck &&
(0, limits_1.isLimitReached)('ConcurrentPRs', prConfig) &&
!config.isVulnerabilityAlert) {
logger_1.logger.debug('Skipping PR - limit reached');
return { type: 'without-pr', prBlockedBy: 'RateLimited' };
}
pr = await platform_1.platform.createPr({
sourceBranch: branchName,
targetBranch: config.baseBranch,
prTitle,
prBody,
labels: (0, labels_1.prepareLabels)(config),
platformPrOptions: getPlatformPrOptions(config),
draftPR: !!config.draftPR,
milestone: config.milestone,
});
(0, limits_1.incCountValue)('ConcurrentPRs');
(0, limits_1.incCountValue)('HourlyPRs');
logger_1.logger.info({ pr: pr?.number, prTitle }, 'PR created');
}
catch (err) {
logger_1.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_1.logger.warn('A pull requests already exists');
return { type: 'without-pr', prBlockedBy: 'Error' };
}
if (err.statusCode === 502) {
logger_1.logger.warn({ branch: branchName }, 'Deleting branch due to server error');
await scm_1.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_1.platform.massageMarkdown(content);
logger_1.logger.debug('Adding branch automerge failure message to PR');
if (global_1.GlobalConfig.get('dryRun')) {
logger_1.logger.info(`DRY-RUN: Would add comment to PR #${pr.number}`);
}
else {
await (0, comment_1.ensureComment)({
number: pr.number,
topic,
content,
});
}
}
// Skip assign and review if automerging PR
if (pr) {
if (config.automerge &&
!config.assignAutomerge &&
(await getBranchStatus()) !== 'red') {
logger_1.logger.debug(`Skipping assignees and reviewers as automerge=${config.automerge}`);
}
else {
await (0, participants_1.addParticipants)(config, pr);
}
(0, pr_cache_1.setPrCache)(branchName, prBodyFingerprint, true);
logger_1.logger.debug(`Created Pull Request #${pr.number}`);
return { type: 'with-pr', pr };
}
}
catch (err) {
if (err instanceof external_host_error_1.ExternalHostError ||
err.message === error_messages_1.REPOSITORY_CHANGED ||
err.message === error_messages_1.PLATFORM_RATE_LIMIT_EXCEEDED ||
err.message === error_messages_1.PLATFORM_INTEGRATION_UNAUTHORIZED) {
logger_1.logger.debug('Passing error up');
throw err;
}
logger_1.logger.warn({ err, prTitle }, 'Failed to ensure PR');
}
if (existingPr) {
return { type: 'with-pr', pr: existingPr };
}
return { type: 'without-pr', prBlockedBy: 'Error' };
}
//# sourceMappingURL=index.js.map