renovate
Version:
Automated dependency updates. Flexible so you don't need to be.
487 lines • 25.1 kB
JavaScript
;
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)(` - \\[ \\] ${getMarkdownComment('unlimit-branch=([^\\s]+)')}`, 'g');
const pendingApprovalRe = (0, regex_1.regEx)(` - \\[ \\] ${getMarkdownComment('approve-branch=([^\\s]+)')}`, 'g');
const generalBranchRe = (0, regex_1.regEx)(` ${getMarkdownComment('([a-zA-Z]+)-branch=([^\\s]+)')}`);
const markedBranchesRe = (0, regex_1.regEx)(` - \\[x\\] ${getMarkdownComment('([a-zA-Z]+)-branch=([^\\s]+)')}`, 'g');
const approveAllPendingPrs = 'approve-all-pending-prs';
const createAllRateLimitedPrs = 'create-all-rate-limited-prs';
const createConfigMigrationPr = 'create-config-migration-pr';
const configMigrationPrInfo = 'config-migration-pr-info';
const rebaseAllOpenPrs = 'rebase-all-open-prs';
function getMarkdownComment(comment) {
return `<!-- ${comment} -->`;
}
function isBoxChecked(issueBody, type) {
return issueBody.includes(getCheckbox(type, true));
}
function isBoxUnchecked(issueBody, type) {
return issueBody.includes(getCheckbox(type));
}
function getCheckbox(type, checked = false) {
return ` - [${checked ? 'x' : ' '}] ${getMarkdownComment(type)}`;
}
function checkOpenAllRateLimitedPR(issueBody) {
return isBoxChecked(issueBody, createAllRateLimitedPrs);
}
function checkApproveAllPendingPR(issueBody) {
return isBoxChecked(issueBody, approveAllPendingPrs);
}
function checkRebaseAll(issueBody) {
return isBoxChecked(issueBody, rebaseAllOpenPrs);
}
function getConfigMigrationCheckboxState(issueBody) {
if (issueBody.includes(getMarkdownComment(configMigrationPrInfo))) {
return 'migration-pr-exists';
}
if (isBoxChecked(issueBody, createConfigMigrationPr)) {
return 'checked';
}
if (isBoxUnchecked(issueBody, createConfigMigrationPr)) {
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 = getCheckbox(`${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 getBranchesListMd(branches, predicate, title, description, listItemType = 'approvePr', bulkComment, bulkMessage, bulkIcon) {
const filteredBranches = branches.filter(predicate);
if (filteredBranches.length === 0) {
return '';
}
let result = `## ${title}\n\n${description}\n\n`;
result += `${filteredBranches
.map((branch) => getListItem(branch, listItemType))
.join('')}`;
if (bulkComment && bulkMessage && filteredBranches.length > 1) {
result += getCheckbox(bulkComment);
result += `${bulkIcon ? bulkIcon + ' ' : ''}**${bulkMessage}**${bulkIcon ? ' ' + bulkIcon : ''}\n`;
}
return result + '\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' +
getMarkdownComment(configMigrationPrInfo) +
` See Config Migration PR: #${configMigrationRes.prNumber}.\n\n`;
}
else if (configMigrationRes?.result === 'pr-modified') {
issueBody +=
'## Config Migration Needed (error)\n\n' +
getMarkdownComment(configMigrationPrInfo) +
` 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' +
getCheckbox(createConfigMigrationPr) +
' 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
? ''
: ''} |\n`;
}
}
issueBody += '\n';
}
if (config.dependencyDashboardReportAbandonment) {
issueBody += getAbandonedPackagesMd(packageFiles);
}
issueBody += getBranchesListMd(branches, (branch) => branch.result === 'needs-approval', 'Pending Approval', 'The following branches are pending approval. To create them, click on a checkbox below.', 'approve', approveAllPendingPrs, 'Create all pending approval PRs at once', '🔐');
issueBody += getBranchesListMd(branches, (branch) => branch.result === 'minimum-group-size-not-met', 'Group Size Not Met', 'The following branches have not met their minimum group size. To create them, click on a checkbox below.', 'approveGroup');
issueBody += getBranchesListMd(branches, (branch) => branch.result === 'not-scheduled', 'Awaiting Schedule', 'The following updates are awaiting their schedule. To get an update now, click on a checkbox below.', 'unschedule');
issueBody += getBranchesListMd(branches, (branch) => branch.result === 'branch-limit-reached' ||
branch.result === 'pr-limit-reached' ||
branch.result === 'commit-limit-reached', 'Rate-Limited', 'The following updates are currently rate-limited. To force their creation now, click on a checkbox below.', 'unlimit', createAllRateLimitedPrs, 'Create all rate-limited PRs at once', '🔐');
issueBody += getBranchesListMd(branches, (branch) => branch.result === 'error', 'Errored', 'The following updates encountered an error and will be retried. To force a retry now, click on a checkbox below.', 'retry');
issueBody += getBranchesListMd(branches, (branch) => branch.result === 'needs-pr-approval', 'PR Creation Approval Required', 'The following branches exist but PR creation requires approval. To approve PR creation, click on a checkbox below.');
issueBody += getBranchesListMd(branches, (branch) => branch.result === 'pr-edited', 'Edited/Blocked', 'The following updates have been manually edited so Renovate will no longer make changes. To discard all commits and start over, click on a checkbox below.', 'rebase');
issueBody += getBranchesListMd(branches, (branch) => branch.result === 'pending', 'Pending Status Checks', 'The following updates await pending status checks. To force their creation now, click on a checkbox below.');
issueBody += getBranchesListMd(branches, (branch) => branch.prBlockedBy === 'BranchAutomerge', 'Pending Branch Automerge', 'The following updates await pending status checks before automerging. To abort the branch automerge and create a PR instead, click on a checkbox below.');
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',
'minimum-group-size-not-met',
];
const inProgress = branches.filter((branch) => !otherRes.includes(branch.result) &&
branch.prBlockedBy !== 'BranchAutomerge');
issueBody += getBranchesListMd(inProgress, (branch) => !!branch.prBlockedBy || !branch.prNo, 'Other Branches', 'The following updates are pending. To force the creation of a PR, click on a checkbox below.', 'other');
issueBody += getBranchesListMd(inProgress, (branch) => branch.prNo && !branch.prBlockedBy, 'Open', 'The following updates have all been created. To force a retry/rebase of any, click on a checkbox below.', 'rebase', rebaseAllOpenPrs, 'Click on this checkbox to rebase all open PRs at once');
issueBody += getBranchesListMd(branches, (branch) => branch.result === 'already-existed', 'Ignored or Blocked', 'The following updates are blocked by an existing closed PR. To recreate the PR, click on a checkbox below.', 'recreate');
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 = getCheckbox(`${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, config.rebaseLabel),
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