UNPKG

renovate

Version:

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

562 lines 26.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.readDashboardBody = readDashboardBody; exports.ensureDependencyDashboard = ensureDependencyDashboard; exports.getAbandonedPackagesMd = getAbandonedPackagesMd; exports.getDashboardMarkdownVulnerabilities = getDashboardMarkdownVulnerabilities; const tslib_1 = require("tslib"); const is_1 = tslib_1.__importDefault(require("@sindresorhus/is")); const luxon_1 = require("luxon"); const global_1 = require("../../config/global"); const logger_1 = require("../../logger"); const platform_1 = require("../../modules/platform"); const array_1 = require("../../util/array"); const regex_1 = require("../../util/regex"); const string_1 = require("../../util/string"); const template = tslib_1.__importStar(require("../../util/template")); const common_1 = require("./common"); const errors_warnings_1 = require("./errors-warnings"); const package_files_1 = require("./package-files"); const vulnerabilities_1 = require("./process/vulnerabilities"); const rateLimitedRe = (0, regex_1.regEx)(' - \\[ \\] <!-- unlimit-branch=([^\\s]+) -->', 'g'); const pendingApprovalRe = (0, regex_1.regEx)(' - \\[ \\] <!-- approve-branch=([^\\s]+) -->', 'g'); const generalBranchRe = (0, regex_1.regEx)(' <!-- ([a-zA-Z]+)-branch=([^\\s]+) -->'); const markedBranchesRe = (0, regex_1.regEx)(' - \\[x\\] <!-- ([a-zA-Z]+)-branch=([^\\s]+) -->', 'g'); function checkOpenAllRateLimitedPR(issueBody) { return issueBody.includes(' - [x] <!-- create-all-rate-limited-prs -->'); } function checkApproveAllPendingPR(issueBody) { return issueBody.includes(' - [x] <!-- approve-all-pending-prs -->'); } function checkRebaseAll(issueBody) { return issueBody.includes(' - [x] <!-- rebase-all-open-prs -->'); } function getConfigMigrationCheckboxState(issueBody) { if (issueBody.includes('<!-- config-migration-pr-info -->')) { return 'migration-pr-exists'; } if (issueBody.includes(' - [x] <!-- create-config-migration-pr -->')) { return 'checked'; } if (issueBody.includes(' - [ ] <!-- create-config-migration-pr -->')) { return 'unchecked'; } return 'no-checkbox'; } function selectAllRelevantBranches(issueBody) { const checkedBranches = []; if (checkOpenAllRateLimitedPR(issueBody)) { for (const match of issueBody.matchAll(rateLimitedRe)) { checkedBranches.push(match[0]); } } if (checkApproveAllPendingPR(issueBody)) { for (const match of issueBody.matchAll(pendingApprovalRe)) { checkedBranches.push(match[0]); } } return checkedBranches; } function getAllSelectedBranches(issueBody, dependencyDashboardChecks) { const allRelevantBranches = selectAllRelevantBranches(issueBody); for (const branch of allRelevantBranches) { const [, type, branchName] = generalBranchRe.exec(branch); dependencyDashboardChecks[branchName] = type; } return dependencyDashboardChecks; } function getCheckedBranches(issueBody) { let dependencyDashboardChecks = {}; for (const [, type, branchName] of issueBody.matchAll(markedBranchesRe)) { dependencyDashboardChecks[branchName] = type; } dependencyDashboardChecks = getAllSelectedBranches(issueBody, dependencyDashboardChecks); return dependencyDashboardChecks; } function parseDashboardIssue(issueBody) { const dependencyDashboardChecks = getCheckedBranches(issueBody); const dependencyDashboardRebaseAllOpen = checkRebaseAll(issueBody); const dependencyDashboardAllPending = checkApproveAllPendingPR(issueBody); const dependencyDashboardAllRateLimited = checkOpenAllRateLimitedPR(issueBody); dependencyDashboardChecks.configMigrationCheckboxState = getConfigMigrationCheckboxState(issueBody); return { dependencyDashboardChecks, dependencyDashboardRebaseAllOpen, dependencyDashboardAllPending, dependencyDashboardAllRateLimited, }; } async function readDashboardBody(config) { let dashboardChecks = { dependencyDashboardChecks: {}, dependencyDashboardAllPending: false, dependencyDashboardRebaseAllOpen: false, dependencyDashboardAllRateLimited: false, }; const stringifiedConfig = JSON.stringify(config); if (config.dependencyDashboard === true || stringifiedConfig.includes('"dependencyDashboardApproval":true') || stringifiedConfig.includes('"prCreation":"approval"')) { config.dependencyDashboardTitle = config.dependencyDashboardTitle ?? `Dependency Dashboard`; const issue = await platform_1.platform.findIssue(config.dependencyDashboardTitle); if (issue) { config.dependencyDashboardIssue = issue.number; dashboardChecks = parseDashboardIssue(issue.body ?? ''); } } if (config.checkedBranches) { const checkedBranchesRec = Object.fromEntries(config.checkedBranches.map((branchName) => [branchName, 'global-config'])); dashboardChecks.dependencyDashboardChecks = { ...dashboardChecks.dependencyDashboardChecks, ...checkedBranchesRec, }; } Object.assign(config, dashboardChecks); } function getListItem(branch, type) { let item = ' - [ ] '; item += `<!-- ${type}-branch=${branch.branchName} -->`; if (branch.prNo) { // TODO: types (#22198) item += `[${branch.prTitle}](../pull/${branch.prNo})`; } else { item += branch.prTitle; } const uniquePackages = [ // TODO: types (#22198) ...new Set(branch.upgrades.map((upgrade) => `\`${upgrade.depName}\``)), ]; if (uniquePackages.length < 2) { return item + '\n'; } return item + ' (' + uniquePackages.join(', ') + ')\n'; } function appendRepoProblems(config, issueBody) { let newIssueBody = issueBody; const repoProblems = (0, common_1.extractRepoProblems)(config.repository); if (repoProblems.size) { newIssueBody += '## Repository problems\n\n'; const repoProblemsHeader = config.customizeDashboard?.repoProblemsHeader ?? 'Renovate tried to run on this repository, but found these problems.'; newIssueBody += template.compile(repoProblemsHeader, config) + '\n\n'; for (const repoProblem of repoProblems) { newIssueBody += ` - ${repoProblem}\n`; } newIssueBody += '\n'; } return newIssueBody; } async function ensureDependencyDashboard(config, allBranches, packageFiles = {}, configMigrationRes) { logger_1.logger.debug('ensureDependencyDashboard()'); if (config.mode === 'silent') { logger_1.logger.debug('Dependency Dashboard issue is not created, updated or closed when mode=silent'); return; } // legacy/migrated issue const reuseTitle = 'Update Dependencies (Renovate Bot)'; const branches = allBranches.filter((branch) => branch.result !== 'automerged' && !branch.upgrades?.every((upgrade) => upgrade.remediationNotPossible)); if (!(config.dependencyDashboard === true || config.dependencyDashboardApproval === true || config.packageRules?.some((rule) => rule.dependencyDashboardApproval) === true || branches.some((branch) => !!branch.dependencyDashboardApproval || !!branch.dependencyDashboardPrApproval))) { if (global_1.GlobalConfig.get('dryRun')) { logger_1.logger.info({ title: config.dependencyDashboardTitle }, 'DRY-RUN: Would close Dependency Dashboard'); } else { logger_1.logger.debug('Closing Dependency Dashboard'); await platform_1.platform.ensureIssueClosing(config.dependencyDashboardTitle); } return; } // istanbul ignore if if (config.repoIsOnboarded === false) { logger_1.logger.debug('Repo is onboarding - skipping dependency dashboard'); return; } logger_1.logger.debug('Ensuring Dependency Dashboard'); // Check packageFiles for any deprecations let hasDeprecations = false; const deprecatedPackages = {}; logger_1.logger.debug('Checking packageFiles for deprecated packages'); if (is_1.default.nonEmptyObject(packageFiles)) { for (const [manager, fileNames] of Object.entries(packageFiles)) { for (const fileName of fileNames) { for (const dep of fileName.deps) { const name = dep.packageName ?? dep.depName; const hasReplacement = !!dep.updates?.find((updates) => updates.updateType === 'replacement'); if (name && (dep.deprecationMessage ?? hasReplacement)) { hasDeprecations = true; deprecatedPackages[manager] ??= {}; deprecatedPackages[manager][name] ??= hasReplacement; } } } } } const hasBranches = is_1.default.nonEmptyArray(branches); if (config.dependencyDashboardAutoclose && !hasBranches && !hasDeprecations) { if (global_1.GlobalConfig.get('dryRun')) { logger_1.logger.info({ title: config.dependencyDashboardTitle }, 'DRY-RUN: Would close Dependency Dashboard'); } else { logger_1.logger.debug('Closing Dependency Dashboard'); await platform_1.platform.ensureIssueClosing(config.dependencyDashboardTitle); } return; } let issueBody = ''; if (config.dependencyDashboardHeader?.length) { issueBody += template.compile(config.dependencyDashboardHeader, config) + '\n\n'; } if (configMigrationRes.result === 'pr-exists') { issueBody += '## Config Migration Needed\n\n' + `<!-- config-migration-pr-info --> See Config Migration PR: #${configMigrationRes.prNumber}.\n\n`; } else if (configMigrationRes?.result === 'pr-modified') { issueBody += '## Config Migration Needed (error)\n\n' + `<!-- config-migration-pr-info --> The Config Migration branch exists but has been modified by another user. Renovate will not push to this branch unless it is first deleted. \n\n See Config Migration PR: #${configMigrationRes.prNumber}.\n\n`; } else if (configMigrationRes?.result === 'add-checkbox') { issueBody += '## Config Migration Needed\n\n' + ' - [ ] <!-- create-config-migration-pr --> Select this checkbox to let Renovate create an automated Config Migration PR.' + '\n\n'; } issueBody = appendRepoProblems(config, issueBody); if (hasDeprecations) { issueBody += '> ⚠ **Warning**\n> \n'; issueBody += 'These dependencies are deprecated:\n\n'; issueBody += '| Datasource | Name | Replacement PR? |\n'; issueBody += '|------------|------|--------------|\n'; for (const manager of Object.keys(deprecatedPackages).sort()) { const deps = deprecatedPackages[manager]; for (const depName of Object.keys(deps).sort()) { const hasReplacement = deps[depName]; issueBody += `| ${manager} | \`${depName}\` | ${hasReplacement ? '![Available](https://img.shields.io/badge/available-green?style=flat-square)' : '![Unavailable](https://img.shields.io/badge/unavailable-orange?style=flat-square)'} |\n`; } } issueBody += '\n'; } if (config.dependencyDashboardReportAbandonment) { issueBody += getAbandonedPackagesMd(packageFiles); } const pendingApprovals = branches.filter((branch) => branch.result === 'needs-approval'); if (pendingApprovals.length) { issueBody += '## Pending Approval\n\n'; issueBody += `These branches will be created by Renovate only once you click their checkbox below.\n\n`; for (const branch of pendingApprovals) { issueBody += getListItem(branch, 'approve'); } if (pendingApprovals.length > 1) { issueBody += ' - [ ] '; issueBody += '<!-- approve-all-pending-prs -->'; issueBody += '🔐 **Create all pending approval PRs at once** 🔐\n'; } issueBody += '\n'; } const awaitingSchedule = branches.filter((branch) => branch.result === 'not-scheduled'); if (awaitingSchedule.length) { issueBody += '## Awaiting Schedule\n\n'; issueBody += 'These updates are awaiting their schedule. Click on a checkbox to get an update now.\n\n'; for (const branch of awaitingSchedule) { issueBody += getListItem(branch, 'unschedule'); } issueBody += '\n'; } const rateLimited = branches.filter((branch) => branch.result === 'branch-limit-reached' || branch.result === 'pr-limit-reached' || branch.result === 'commit-limit-reached'); if (rateLimited.length) { issueBody += '## Rate-Limited\n\n'; issueBody += 'These updates are currently rate-limited. Click on a checkbox below to force their creation now.\n\n'; for (const branch of rateLimited) { issueBody += getListItem(branch, 'unlimit'); } if (rateLimited.length > 1) { issueBody += ' - [ ] '; issueBody += '<!-- create-all-rate-limited-prs -->'; issueBody += '🔐 **Create all rate-limited PRs at once** 🔐\n'; } issueBody += '\n'; } const errorList = branches.filter((branch) => branch.result === 'error'); if (errorList.length) { issueBody += '## Errored\n\n'; issueBody += 'These updates encountered an error and will be retried. Click on a checkbox below to force a retry now.\n\n'; for (const branch of errorList) { issueBody += getListItem(branch, 'retry'); } issueBody += '\n'; } const awaitingPr = branches.filter((branch) => branch.result === 'needs-pr-approval'); if (awaitingPr.length) { issueBody += '## PR Creation Approval Required\n\n'; issueBody += "These branches exist but PRs won't be created until you approve them by clicking on a checkbox.\n\n"; for (const branch of awaitingPr) { issueBody += getListItem(branch, 'approvePr'); } issueBody += '\n'; } const prEdited = branches.filter((branch) => branch.result === 'pr-edited'); if (prEdited.length) { issueBody += '## Edited/Blocked\n\n'; issueBody += `These updates have been manually edited so Renovate will no longer make changes. To discard all commits and start over, click on a checkbox.\n\n`; for (const branch of prEdited) { issueBody += getListItem(branch, 'rebase'); } issueBody += '\n'; } const prPending = branches.filter((branch) => branch.result === 'pending'); if (prPending.length) { issueBody += '## Pending Status Checks\n\n'; issueBody += `These updates await pending status checks. To force their creation now, click the checkbox below.\n\n`; for (const branch of prPending) { issueBody += getListItem(branch, 'approvePr'); } issueBody += '\n'; } const prPendingBranchAutomerge = branches.filter((branch) => branch.prBlockedBy === 'BranchAutomerge'); if (prPendingBranchAutomerge.length) { issueBody += '## Pending Branch Automerge\n\n'; issueBody += `These updates await pending status checks before automerging. Click on a checkbox to abort the branch automerge, and create a PR instead.\n\n`; for (const branch of prPendingBranchAutomerge) { issueBody += getListItem(branch, 'approvePr'); } issueBody += '\n'; } const warn = (0, errors_warnings_1.getDepWarningsDashboard)(packageFiles, config); if (warn) { issueBody += warn; issueBody += '\n'; } const otherRes = [ 'pending', 'needs-approval', 'needs-pr-approval', 'not-scheduled', 'pr-limit-reached', 'commit-limit-reached', 'branch-limit-reached', 'already-existed', 'error', 'automerged', 'pr-edited', ]; let inProgress = branches.filter((branch) => !otherRes.includes(branch.result) && branch.prBlockedBy !== 'BranchAutomerge'); const otherBranches = inProgress.filter((branch) => !!branch.prBlockedBy || !branch.prNo); // istanbul ignore if if (otherBranches.length) { issueBody += '## Other Branches\n\n'; issueBody += `These updates are pending. To force PRs open, click the checkbox below.\n\n`; for (const branch of otherBranches) { issueBody += getListItem(branch, 'other'); } issueBody += '\n'; } inProgress = inProgress.filter((branch) => branch.prNo && !branch.prBlockedBy); if (inProgress.length) { issueBody += '## Open\n\n'; issueBody += 'These updates have all been created already. Click a checkbox below to force a retry/rebase of any.\n\n'; for (const branch of inProgress) { issueBody += getListItem(branch, 'rebase'); } if (inProgress.length > 2) { issueBody += ' - [ ] '; issueBody += '<!-- rebase-all-open-prs -->'; issueBody += '**Click on this checkbox to rebase all open PRs at once**'; issueBody += '\n'; } issueBody += '\n'; } const alreadyExisted = branches.filter((branch) => branch.result === 'already-existed'); if (alreadyExisted.length) { issueBody += '## Ignored or Blocked\n\n'; issueBody += 'These are blocked by an existing closed PR and will not be recreated unless you click a checkbox below.\n\n'; for (const branch of alreadyExisted) { issueBody += getListItem(branch, 'recreate'); } issueBody += '\n'; } if (!hasBranches) { issueBody += 'This repository currently has no open or pending branches.\n\n'; } // add CVE section issueBody += await getDashboardMarkdownVulnerabilities(config, packageFiles); // fit the detected dependencies section const footer = getFooter(config); issueBody += package_files_1.PackageFiles.getDashboardMarkdown(platform_1.platform.maxBodyLength() - issueBody.length - footer.length); issueBody += footer; if (config.dependencyDashboardIssue) { // If we're not changing the dashboard issue then we can skip checking if the user changed it // The cached issue we get back here will reflect its state at the _start_ of our run const cachedIssue = await platform_1.platform.getIssue?.(config.dependencyDashboardIssue); if (cachedIssue?.body === issueBody) { logger_1.logger.debug('No changes to dependency dashboard issue needed'); return; } // Skip cache when getting the issue to ensure we get the latest body, // including any updates the user made after we started the run const updatedIssue = await platform_1.platform.getIssue?.(config.dependencyDashboardIssue, false); if (updatedIssue) { const { dependencyDashboardChecks } = parseDashboardIssue((0, string_1.coerceString)(updatedIssue.body)); for (const branchName of Object.keys(config.dependencyDashboardChecks)) { delete dependencyDashboardChecks[branchName]; } for (const branchName of Object.keys(dependencyDashboardChecks)) { const checkText = `- [ ] <!-- ${dependencyDashboardChecks[branchName]}-branch=${branchName} -->`; issueBody = issueBody.replace(checkText, checkText.replace('[ ]', '[x]')); } } } if (global_1.GlobalConfig.get('dryRun')) { logger_1.logger.info({ title: config.dependencyDashboardTitle }, 'DRY-RUN: Would ensure Dependency Dashboard'); } else { await platform_1.platform.ensureIssue({ title: config.dependencyDashboardTitle, reuseTitle, body: platform_1.platform.massageMarkdown(issueBody), labels: config.dependencyDashboardLabels, confidential: config.confidential, }); } } function getAbandonedPackagesMd(packageFiles) { const abandonedPackages = {}; let abandonedCount = 0; for (const [manager, managerPackageFiles] of Object.entries(packageFiles)) { for (const packageFile of managerPackageFiles) { for (const dep of (0, array_1.coerceArray)(packageFile.deps)) { if (dep.depName && dep.isAbandoned) { abandonedCount++; abandonedPackages[manager] = abandonedPackages[manager] || {}; abandonedPackages[manager][dep.depName] = dep.mostRecentTimestamp; } } } } if (abandonedCount === 0) { return ''; } let abandonedMd = '> ℹ **Note**\n> \n'; abandonedMd += 'These dependencies have not received updates for an extended period and may be unmaintained:\n\n'; abandonedMd += '<details>\n'; abandonedMd += `<summary>View abandoned dependencies (${abandonedCount})</summary>\n\n`; abandonedMd += '| Datasource | Name | Last Updated |\n'; abandonedMd += '|------------|------|-------------|\n'; for (const manager of Object.keys(abandonedPackages).sort()) { const deps = abandonedPackages[manager]; for (const depName of Object.keys(deps).sort()) { const mostRecentTimestamp = deps[depName]; const formattedDate = mostRecentTimestamp ? luxon_1.DateTime.fromISO(mostRecentTimestamp).toFormat('yyyy-MM-dd') : 'unknown'; abandonedMd += `| ${manager} | \`${depName}\` | \`${formattedDate}\` |\n`; } } abandonedMd += '\n</details>\n\n'; abandonedMd += 'Packages are marked as abandoned when they exceed the [`abandonmentThreshold`](https://docs.renovatebot.com/configuration-options/#abandonmentthreshold) since their last release.\n'; abandonedMd += 'Unlike deprecated packages with official notices, abandonment is detected by release inactivity.\n\n'; return abandonedMd + '\n'; } function getFooter(config) { let footer = ''; if (config.dependencyDashboardFooter?.length) { footer += '---\n' + template.compile(config.dependencyDashboardFooter, config) + '\n'; } return footer; } async function getDashboardMarkdownVulnerabilities(config, packageFiles) { let result = ''; if (is_1.default.nullOrUndefined(config.dependencyDashboardOSVVulnerabilitySummary) || config.dependencyDashboardOSVVulnerabilitySummary === 'none') { return result; } result += '## Vulnerabilities\n\n'; const vulnerabilityFetcher = await vulnerabilities_1.Vulnerabilities.create(); const vulnerabilities = await vulnerabilityFetcher.fetchVulnerabilities(config, packageFiles); if (vulnerabilities.length === 0) { result += 'Renovate has not found any CVEs on [osv.dev](https://osv.dev).\n\n'; return result; } const unresolvedVulnerabilities = vulnerabilities.filter((value) => is_1.default.nullOrUndefined(value.fixedVersion)); const resolvedVulnerabilitiesLength = vulnerabilities.length - unresolvedVulnerabilities.length; result += `\`${resolvedVulnerabilitiesLength}\`/\`${vulnerabilities.length}\``; if (is_1.default.truthy(config.osvVulnerabilityAlerts)) { result += ' CVEs have Renovate fixes.\n'; } else { result += ' CVEs have possible Renovate fixes.\nSee [`osvVulnerabilityAlerts`](https://docs.renovatebot.com/configuration-options/#osvvulnerabilityalerts) to allow Renovate to supply fixes.\n'; } let renderedVulnerabilities; switch (config.dependencyDashboardOSVVulnerabilitySummary) { // filter vulnerabilities to display based on configuration case 'unresolved': renderedVulnerabilities = unresolvedVulnerabilities; break; default: renderedVulnerabilities = vulnerabilities; } const managerRecords = {}; for (const vulnerability of renderedVulnerabilities) { const { manager, packageFile } = vulnerability.packageFileConfig; if (is_1.default.nullOrUndefined(managerRecords[manager])) { managerRecords[manager] = {}; } if (is_1.default.nullOrUndefined(managerRecords[manager][packageFile])) { managerRecords[manager][packageFile] = {}; } if (is_1.default.nullOrUndefined(managerRecords[manager][packageFile][vulnerability.packageName])) { managerRecords[manager][packageFile][vulnerability.packageName] = []; } managerRecords[manager][packageFile][vulnerability.packageName].push(vulnerability); } for (const [manager, packageFileRecords] of Object.entries(managerRecords)) { result += `<details><summary>${manager}</summary>\n<blockquote>\n\n`; for (const [packageFile, packageNameRecords] of Object.entries(packageFileRecords)) { result += `<details><summary>${packageFile}</summary>\n<blockquote>\n\n`; for (const [packageName, cves] of Object.entries(packageNameRecords)) { result += `<details><summary>${packageName}</summary>\n<blockquote>\n\n`; for (const vul of cves) { const id = vul.vulnerability.id; const suffix = is_1.default.nonEmptyString(vul.fixedVersion) ? ` (fixed in ${vul.fixedVersion})` : ''; result += `- [${id}](https://osv.dev/vulnerability/${id})${suffix}\n`; } result += `</blockquote>\n</details>\n\n`; } result += `</blockquote>\n</details>\n\n`; } result += `</blockquote>\n</details>\n\n`; } return result; } //# sourceMappingURL=dependency-dashboard.js.map