Files
renovate/lib/workers/repository/dependency-dashboard.ts
renovate[bot] 9d74625d93 build(deps): update dependency prettier to v3.9.6 (main) (#45815)
* build(deps): update dependency prettier to v3.9.6

| datasource | package  | from  | to    |
| ---------- | -------- | ----- | ----- |
| npm        | prettier | 3.8.1 | 3.9.6 |

* chore: prettier fixes

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Kriese <michael.kriese@mend.io>
2026-09-09 13:50:03 +00:00

864 lines
29 KiB
TypeScript

import {
isNonEmptyArray,
isNonEmptyObject,
isNonEmptyString,
isNullOrUndefined,
isTruthy,
} from '@sindresorhus/is';
import { DateTime } from 'luxon';
import { GlobalConfig } from '../../config/global.ts';
import type { RenovateConfig } from '../../config/types.ts';
import { logger } from '../../logger/index.ts';
import type { PackageFile } from '../../modules/manager/types.ts';
import { platform } from '../../modules/platform/index.ts';
import { coerceArray } from '../../util/array.ts';
import { emojify } from '../../util/emoji.ts';
import { regEx } from '../../util/regex.ts';
import { coerceString } from '../../util/string.ts';
import * as template from '../../util/template/index.ts';
import type { BranchConfig, SelectAllConfig } from '../types.ts';
import { extractRepoProblems, replacementAlreadyExists } from './common.ts';
import type { ConfigMigrationResult } from './config-migration/index.ts';
import { getDepWarningsDashboard } from './errors-warnings.ts';
import { PackageFiles } from './package-files.ts';
import type { Vulnerability } from './process/types.ts';
import { Vulnerabilities } from './process/vulnerabilities.ts';
import type {
DependencyDashboardCheck,
DependencyDashboardListItemType,
} from './types.ts';
interface DependencyDashboard {
dependencyDashboardChecks: Record<string, string>;
dependencyDashboardRebaseAllOpen: boolean;
dependencyDashboardAllPending: boolean;
dependencyDashboardAllRateLimited: boolean;
dependencyDashboardAllAwaitingSchedule: boolean;
}
const rateLimitedRe = regEx(
` - \\[ \\] ${getMarkdownComment('unlimit-branch=([^\\s]+)')}`,
'g',
);
const pendingApprovalRe = regEx(
` - \\[ \\] ${getMarkdownComment('approve-branch=([^\\s]+)')}`,
'g',
);
const awaitingScheduleRe = regEx(
` - \\[ \\] ${getMarkdownComment('unschedule-branch=([^\\s]+)')}`,
'g',
);
const generalBranchRe = regEx(
` ${getMarkdownComment('([a-zA-Z]+)-branch=([^\\s]+)')}`,
);
const markedBranchesRe = regEx(
` - \\[x\\] ${getMarkdownComment('([a-zA-Z]+)-branch=([^\\s]+)')}`,
'g',
);
const approveAllPendingPrs = 'approve-all-pending-prs';
const createAllRateLimitedPrs = 'create-all-rate-limited-prs';
const createAllAwaitingSchedulePrs = 'create-all-awaiting-schedule-prs';
const createConfigMigrationPr = 'create-config-migration-pr';
const configMigrationPrInfo = 'config-migration-pr-info';
const rebaseAllOpenPrs = 'rebase-all-open-prs';
function getMarkdownComment(comment: string): string {
return `<!-- ${comment} -->`;
}
function isBoxChecked(issueBody: string, type: string): boolean {
return issueBody.includes(getCheckbox(type, true));
}
function isBoxUnchecked(issueBody: string, type: string): boolean {
return issueBody.includes(getCheckbox(type));
}
function getCheckbox(type: string, checked = false): string {
return ` - [${checked ? 'x' : ' '}] ${getMarkdownComment(type)}`;
}
function checkOpenAllRateLimitedPR(issueBody: string): boolean {
return isBoxChecked(issueBody, createAllRateLimitedPrs);
}
function checkOpenAllAwaitingSchedulePR(issueBody: string): boolean {
return isBoxChecked(issueBody, createAllAwaitingSchedulePrs);
}
function checkApproveAllPendingPR(issueBody: string): boolean {
return isBoxChecked(issueBody, approveAllPendingPrs);
}
function checkRebaseAll(issueBody: string): boolean {
return isBoxChecked(issueBody, rebaseAllOpenPrs);
}
function getConfigMigrationCheckboxState(
issueBody: string,
): 'no-checkbox' | 'checked' | 'unchecked' | 'migration-pr-exists' {
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: string): string[] {
const checkedBranches = [];
if (checkOpenAllRateLimitedPR(issueBody)) {
for (const match of issueBody.matchAll(rateLimitedRe)) {
checkedBranches.push(match[0]);
}
}
if (checkOpenAllAwaitingSchedulePR(issueBody)) {
for (const match of issueBody.matchAll(awaitingScheduleRe)) {
checkedBranches.push(match[0]);
}
}
if (checkApproveAllPendingPR(issueBody)) {
for (const match of issueBody.matchAll(pendingApprovalRe)) {
checkedBranches.push(match[0]);
}
}
return checkedBranches;
}
function getAllSelectedBranches(
issueBody: string,
dependencyDashboardChecks: Record<string, string>,
): Record<string, string> {
const allRelevantBranches = selectAllRelevantBranches(issueBody);
for (const branch of allRelevantBranches) {
const [, type, branchName] = generalBranchRe.exec(branch)!;
dependencyDashboardChecks[branchName] = type;
}
return dependencyDashboardChecks;
}
function getCheckedBranches(issueBody: string): Record<string, string> {
let dependencyDashboardChecks: Record<string, string> = {};
for (const [, type, branchName] of issueBody.matchAll(markedBranchesRe)) {
dependencyDashboardChecks[branchName] = type;
}
dependencyDashboardChecks = getAllSelectedBranches(
issueBody,
dependencyDashboardChecks,
);
return dependencyDashboardChecks;
}
function parseDashboardIssue(issueBody: string): DependencyDashboard {
const dependencyDashboardChecks = getCheckedBranches(issueBody);
const dependencyDashboardRebaseAllOpen = checkRebaseAll(issueBody);
const dependencyDashboardAllAwaitingSchedule =
checkOpenAllAwaitingSchedulePR(issueBody);
const dependencyDashboardAllPending = checkApproveAllPendingPR(issueBody);
const dependencyDashboardAllRateLimited =
checkOpenAllRateLimitedPR(issueBody);
dependencyDashboardChecks.configMigrationCheckboxState =
getConfigMigrationCheckboxState(issueBody);
return {
dependencyDashboardChecks,
dependencyDashboardRebaseAllOpen,
dependencyDashboardAllAwaitingSchedule,
dependencyDashboardAllPending,
dependencyDashboardAllRateLimited,
};
}
export async function readDashboardBody(
config: SelectAllConfig,
): Promise<void> {
let dashboardChecks: DependencyDashboard = {
dependencyDashboardChecks: {},
dependencyDashboardRebaseAllOpen: false,
dependencyDashboardAllAwaitingSchedule: false,
dependencyDashboardAllPending: 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.findIssue(config.dependencyDashboardTitle);
if (issue) {
config.dependencyDashboardIssue = issue.number;
dashboardChecks = parseDashboardIssue(issue.body ?? '');
}
}
const checkedBranches = GlobalConfig.get('checkedBranches');
if (isNonEmptyArray(checkedBranches)) {
const checkedBranchesRec: Record<string, DependencyDashboardCheck> =
Object.fromEntries(
checkedBranches.map((branchName) => [branchName, 'global-config']),
);
dashboardChecks.dependencyDashboardChecks = {
...dashboardChecks.dependencyDashboardChecks,
...checkedBranchesRec,
};
}
if (GlobalConfig.get('rebaseAllOpenBranches')) {
dashboardChecks.dependencyDashboardRebaseAllOpen = true;
}
Object.assign(config, dashboardChecks);
}
function formatAsMarkdownLink(name: string, url?: string | null): string {
return url ? `[${name}](${url})` : `\`${name}\``;
}
function getListItem(
branch: BranchConfig,
type: DependencyDashboardListItemType,
): string {
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 splitBranchesByCategory(filteredBranches: BranchConfig[]): {
categories: Record<string, BranchConfig[]>;
uncategorized: BranchConfig[];
hasCategorized: boolean;
hasUncategorized: boolean;
} {
const categories: Record<string, BranchConfig[]> = {};
const uncategorized: BranchConfig[] = [];
let hasCategorized = false;
let hasUncategorized = false;
for (const branch of filteredBranches) {
if (branch.dependencyDashboardCategory) {
categories[branch.dependencyDashboardCategory] ??= [];
categories[branch.dependencyDashboardCategory].push(branch);
hasCategorized = true;
continue;
}
uncategorized.push(branch);
hasUncategorized = true;
}
return { categories, uncategorized, hasCategorized, hasUncategorized };
}
function getBranchList(
branches: BranchConfig[],
listItemType: DependencyDashboardListItemType,
): string {
return branches
.map((branch: BranchConfig): string => getListItem(branch, listItemType))
.join('');
}
function getBranchesListMd(
branches: BranchConfig[],
predicate: (
value: BranchConfig,
index: number,
array: BranchConfig[],
) => unknown,
title: string,
description: string,
listItemType: DependencyDashboardListItemType = 'approvePr',
bulkComment?: string,
bulkMessage?: string,
bulkIcon?: '🔐',
): string {
const filteredBranches = branches.filter(predicate);
if (filteredBranches.length === 0) {
return '';
}
let result = `## ${title}\n\n${description}\n\n`;
const { categories, uncategorized, hasCategorized, hasUncategorized } =
splitBranchesByCategory(filteredBranches);
if (hasCategorized) {
for (const [category, branches] of Object.entries(categories).sort(
([keyA], [keyB]) =>
keyA.localeCompare(keyB, undefined, { numeric: true }),
)) {
result = `${result.trimEnd()}\n\n`;
result += `### ${category}\n\n`;
result += getBranchList(branches, listItemType);
}
if (hasUncategorized) {
result = `${result.trimEnd()}\n\n`;
result += `### Others`;
}
}
result = `${result.trimEnd()}\n\n`;
result += getBranchList(uncategorized, listItemType);
if (bulkComment && bulkMessage && filteredBranches.length > 1) {
if (hasCategorized) {
result = `${result.trimEnd()}\n\n`;
result += '### All\n\n';
}
result += getCheckbox(bulkComment);
result += `${bulkIcon ? `${bulkIcon} ` : ''}**${bulkMessage}**${bulkIcon ? ` ${bulkIcon}` : ''}`;
}
return `${result.trimEnd()}\n\n`;
}
function appendRepoProblems(config: RenovateConfig, issueBody: string): string {
let newIssueBody = issueBody;
const repoProblems = 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;
}
export async function ensureDependencyDashboard(
config: SelectAllConfig,
allBranches: BranchConfig[],
packageFiles: Record<string, PackageFile[]> = {},
configMigrationRes: ConfigMigrationResult,
): Promise<void> {
logger.debug('ensureDependencyDashboard()');
if (config.mode === 'silent') {
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 (GlobalConfig.get('dryRun')) {
logger.info(
{ title: config.dependencyDashboardTitle },
'DRY-RUN: Would close Dependency Dashboard',
);
} else {
logger.debug('Closing Dependency Dashboard');
await platform.ensureIssueClosing(config.dependencyDashboardTitle!);
}
return;
}
// istanbul ignore if
if (config.repoIsOnboarded === false) {
logger.debug('Repo is onboarding - skipping dependency dashboard');
return;
}
logger.debug('Ensuring Dependency Dashboard');
// Check packageFiles for any deprecations or replacements
let hasDeprecationsOrReplacements = false;
const deprecatedPackages: Record<
string,
Record<
string,
{ hasActionableReplacement: boolean; sourceUrl?: string | null }
>
> = {};
logger.debug('Checking packageFiles for deprecated or replacement packages');
if (isNonEmptyObject(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 replacementUpdate = dep.updates?.find(
(update) => update.updateType === 'replacement',
);
const hasAnyReplacement = !!replacementUpdate;
const hasActionableReplacement =
hasAnyReplacement &&
!(
replacementUpdate.newName &&
replacementAlreadyExists(
fileName.deps,
dep,
replacementUpdate.newName,
)
);
if (name && (dep.deprecationMessage ?? hasAnyReplacement)) {
hasDeprecationsOrReplacements = true;
deprecatedPackages[manager] ??= {};
deprecatedPackages[manager][name] ??= {
hasActionableReplacement,
sourceUrl: dep.sourceUrl,
};
}
}
}
}
}
const hasBranches = isNonEmptyArray(branches);
if (
config.dependencyDashboardAutoclose &&
!hasBranches &&
!hasDeprecationsOrReplacements
) {
if (GlobalConfig.get('dryRun')) {
logger.info(
{ title: config.dependencyDashboardTitle },
'DRY-RUN: Would close Dependency Dashboard',
);
} else {
logger.debug('Closing Dependency Dashboard');
await 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 (Blocked)\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 (hasDeprecationsOrReplacements) {
issueBody += '## Deprecations / Replacements\n';
issueBody += emojify('> :warning: **Warning**\n> \n');
issueBody +=
'The following dependencies are either deprecated or have replacements available.\n\n';
issueBody += '| Datasource | Package | 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 { hasActionableReplacement, sourceUrl } = deps[depName];
const packageName = formatAsMarkdownLink(depName, sourceUrl);
issueBody += `| ${manager} | ${packageName} | ${
hasActionableReplacement
? '![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(config, 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',
createAllAwaitingSchedulePrs,
'Create all awaiting schedule PRs at once',
'🔐',
);
issueBody += getBranchesListMd(
branches,
(branch) =>
branch.result === 'branch-limit-reached' ||
branch.result === 'pr-limit-reached' ||
branch.result === 'commit-per-run-limit-reached' ||
branch.result === 'commit-hourly-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',
'PR 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.',
'unpend',
);
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 = getDepWarningsDashboard(packageFiles, config);
if (warn) {
issueBody += warn;
issueBody += '\n';
}
const otherRes = [
'pending',
'needs-approval',
'needs-pr-approval',
'not-scheduled',
'pr-limit-reached',
'commit-per-run-limit-reached',
'commit-hourly-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',
'PR Closed (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 += PackageFiles.getDashboardMarkdown(
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.getIssue?.(
config.dependencyDashboardIssue,
);
if (cachedIssue?.body === issueBody) {
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.getIssue?.(
config.dependencyDashboardIssue,
false,
);
if (updatedIssue) {
const { dependencyDashboardChecks } = parseDashboardIssue(
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 (GlobalConfig.get('dryRun')) {
logger.info(
{ title: config.dependencyDashboardTitle },
'DRY-RUN: Would ensure Dependency Dashboard',
);
} else {
await platform.ensureIssue({
title: config.dependencyDashboardTitle!,
reuseTitle,
body: platform.massageMarkdown(issueBody, config.rebaseLabel),
labels: config.dependencyDashboardLabels,
confidential: config.confidential,
});
}
}
export function getAbandonedPackagesMd(
config: RenovateConfig,
packageFiles: Record<string, PackageFile[]>,
): string {
const abandonedPackages: Record<
string,
Record<
string,
{ mostRecentTimestamp?: string | null; sourceUrl?: string | null }
>
> = {};
let abandonedCount = 0;
for (const [manager, managerPackageFiles] of Object.entries(packageFiles)) {
for (const packageFile of managerPackageFiles) {
for (const dep of coerceArray(packageFile.deps)) {
if (dep.depName && dep.isAbandoned) {
abandonedCount++;
abandonedPackages[manager] = abandonedPackages[manager] || {};
abandonedPackages[manager][dep.depName] = {
mostRecentTimestamp: dep.mostRecentTimestamp,
sourceUrl: dep.sourceUrl,
};
}
}
}
}
if (abandonedCount === 0) {
return '';
}
let abandonedMd = '## Abandoned Dependencies\n\n';
abandonedMd +=
'The following 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 += emojify('> :information_source: **Note**\n> \n');
abandonedMd += `Packages are marked as abandoned when they exceed the [\`abandonmentThreshold\`](${GlobalConfig.get('productLinks').documentation}configuration-options/#abandonmentthreshold) since their last release. `;
abandonedMd +=
'Unlike deprecated packages with official notices, abandonment is detected by release inactivity.\n> \n';
abandonedMd += '| Datasource | Package | 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, sourceUrl } = deps[depName];
const formattedDate = mostRecentTimestamp
? DateTime.fromISO(mostRecentTimestamp).toFormat('yyyy-MM-dd')
: 'unknown';
const packageName = formatAsMarkdownLink(depName, sourceUrl);
abandonedMd += `| ${manager} | ${packageName} | \`${formattedDate}\` |\n`;
}
}
abandonedMd += '\n</details>\n\n\n';
return abandonedMd;
}
function getFooter(config: RenovateConfig): string {
let footer = '';
if (config.dependencyDashboardFooter?.length) {
footer += `---\n${template.compile(config.dependencyDashboardFooter, config)}\n`;
}
return footer;
}
export async function getDashboardMarkdownVulnerabilities(
config: RenovateConfig,
packageFiles: Record<string, PackageFile[]>,
): Promise<string> {
let result = '';
if (
isNullOrUndefined(config.dependencyDashboardOSVVulnerabilitySummary) ||
config.dependencyDashboardOSVVulnerabilitySummary === 'none'
) {
return result;
}
result += '## Vulnerabilities\n\n';
const vulnerabilityFetcher = await 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) =>
isNullOrUndefined(value.fixedVersion),
);
const resolvedVulnerabilitiesLength =
vulnerabilities.length - unresolvedVulnerabilities.length;
result += emojify('> :exclamation: **Important**\n> \n');
result += `> \`${resolvedVulnerabilitiesLength}\`/\`${vulnerabilities.length}\``;
if (isTruthy(config.osvVulnerabilityAlerts)) {
result += ' CVEs have Renovate fixes.\n\n';
} else {
result += ` CVEs have possible Renovate fixes.\n> See [\`osvVulnerabilityAlerts\`](${GlobalConfig.get('productLinks').documentation}configuration-options/#osvvulnerabilityalerts) to allow Renovate to supply fixes.\n\n`;
}
let renderedVulnerabilities: Vulnerability[];
switch (config.dependencyDashboardOSVVulnerabilitySummary) {
// filter vulnerabilities to display based on configuration
case 'unresolved':
renderedVulnerabilities = unresolvedVulnerabilities;
break;
default:
renderedVulnerabilities = vulnerabilities;
}
const managerRecords: Record<
string,
Record<string, Record<string, Vulnerability[]>>
> = {};
for (const vulnerability of renderedVulnerabilities) {
const { manager, packageFile } = vulnerability.packageFileConfig;
if (isNullOrUndefined(managerRecords[manager!])) {
managerRecords[manager!] = {};
}
if (isNullOrUndefined(managerRecords[manager!][packageFile])) {
managerRecords[manager!][packageFile] = {};
}
if (
isNullOrUndefined(
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 = isNonEmptyString(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;
}