UNPKG

renovate

Version:

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

435 lines • 21.6 kB
"use strict"; 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, config.rebaseLabel); 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