UNPKG

renovate

Version:

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

210 lines (209 loc) • 11.8 kB
import { regEx } from "../../../../util/regex.js"; import { GlobalConfig } from "../../../../config/global.js"; import { logger } from "../../../../logger/index.js"; import { UpdateTypesOptions } from "../../../../config/types.js"; import { coerceNumber } from "../../../../util/number.js"; import { emojify } from "../../../../util/emoji.js"; //#region lib/workers/repository/onboarding/pr/pr-list.ts /** Priority order when rendering the `SummaryCategory`s in the summary view. * * NOTE that this is not exhaustive - this is only the priority order for rendering, and other categories, such as `other` are placed after. */ const SUMMARY_CATEGORY_PRIORITY_ORDER = ["security", ...UpdateTypesOptions]; function getExpectedPrList(config, branches) { logger.debug("getExpectedPrList()"); logger.trace({ config }); let prDesc = `\n### What to Expect\n\n`; if (!branches.length) return `${prDesc}It looks like your repository dependencies are already up-to-date and no Pull Requests will be necessary right away.\n`; prDesc += `With your current configuration, Renovate will create ${branches.length} Pull Request`; prDesc += branches.length > 1 ? `s:\n\n` : `:\n\n`; for (const branch of branches) { const prTitleRe = regEx(/@([a-z]+\/[a-z]+)/); prDesc += `<details>\n<summary>${branch.prTitle.replace(prTitleRe, "@&#8203;$1")}</summary>\n\n`; if (branch.schedule?.length) prDesc += ` - Schedule: ${JSON.stringify(branch.schedule)}\n`; prDesc += ` - Branch name: \`${branch.branchName}\`\n`; prDesc += branch.baseBranch ? ` - Merge into: \`${branch.baseBranch}\`\n` : ""; const seen = []; for (const upgrade of branch.upgrades) { let text = ""; if (upgrade.updateType === "lockFileMaintenance") text += " - Regenerate lock files to use latest dependency versions"; else { if (upgrade.updateType === "pin") text += " - Pin "; else text += " - Upgrade "; if (upgrade.sourceUrl) text += `[${upgrade.depName}](${upgrade.sourceUrl})`; else text += upgrade.depName.replace(prTitleRe, "@&#8203;$1"); text += upgrade.isLockfileUpdate ? ` to \`${upgrade.newVersion}\`` : ` to \`${upgrade.newDigest ?? upgrade.newValue}\``; text += "\n"; } if (!seen.includes(text)) { prDesc += text; seen.push(text); } } prDesc += "\n\n"; prDesc += "</details>\n\n"; } const prHourlyLimit = coerceNumber(config.prHourlyLimit); const commitHourlyLimit = coerceNumber(config.commitHourlyLimit); if (commitHourlyLimit > 0 && commitHourlyLimit < 5 && commitHourlyLimit < branches.length) prDesc += emojify(`\n\n:children_crossing: Branch creation and rebasing will be limited to maximum ${commitHourlyLimit} per hour, so it doesn't swamp any CI resources or overwhelm the project. See [docs for \`commitHourlyLimit\`](${GlobalConfig.get("productLinks").documentation}configuration-options/#commithourlylimit) for details.\n\n`); else if (prHourlyLimit > 0 && prHourlyLimit < 5 && prHourlyLimit < branches.length) prDesc += emojify(`\n\n:children_crossing: PR creation will be limited to maximum ${prHourlyLimit} per hour, so it doesn't swamp any CI resources or overwhelm the project. See [docs for \`prHourlyLimit\`](${GlobalConfig.get("productLinks").documentation}configuration-options/#prhourlylimit) for details.\n\n`); return prDesc; } function getBranchUpgradeTypes(branch) { if (branch.isVulnerabilityAlert) return /* @__PURE__ */ new Set(["security"]); const types = /* @__PURE__ */ new Set(); for (const upgrade of branch.upgrades) if (upgrade.updateType) types.add(upgrade.updateType); else if (upgrade.isLockfileUpdate) types.add("lockfileUpdate"); else types.add("other"); return types; } function sortBaseBranches(bases) { return [...bases].sort((a, b) => { if (a === b) /* v8 ignore next -- base branches are unique Record keys; equal comparison cannot occur */ return 0; if (a === "") return -1; if (b === "") return 1; return a.localeCompare(b); }); } function describeSecurityGroup(groupBranches) { const firstUpgrade = groupBranches[0].upgrades[0]; const depName = firstUpgrade?.depName ?? groupBranches[0].prTitle ?? ""; const updateType = firstUpgrade?.updateType ?? "unknown"; const packageFiles = groupBranches.map((b) => ({ file: b.packageFile ?? "", manager: b.manager })).filter((f) => f.file); const uniqueManagers = new Set(packageFiles.map((f) => f.manager)); if (packageFiles.length === 0) { const branchManagers = new Set(groupBranches.map((b) => b.manager)); if (branchManagers.size === 1) return `- \`${depName}\`, (${[...branchManagers][0]}, ${updateType})\n`; return `- \`${depName}\`, (${updateType}): ${[...branchManagers].sort().join(", ")}\n`; } if (packageFiles.length === 1) { const file = packageFiles[0].file; const [manager] = uniqueManagers; return `- \`${depName}\`, (${manager}, ${updateType}): \`${file}\`\n`; } if (uniqueManagers.size === 1) return `- \`${depName}\`, (${[...uniqueManagers][0]}, ${updateType}):\n${packageFiles.map(({ file }) => ` - \`${file}\`\n`).join("")}`; return `- \`${depName}\`, (${updateType}):\n${packageFiles.map(({ file, manager }) => ` - \`${file}\` (${manager})\n`).join("")}`; } function collectBranchStats(branches) { const prCountByBase = {}; const tableStats = {}; const presentTypes = /* @__PURE__ */ new Set(); const securityGroups = {}; const seenPrs = /* @__PURE__ */ new Set(); const seenTableKeys = /* @__PURE__ */ new Set(); for (const branch of branches) { const base = branch.baseBranch ?? ""; const { manager, branchName } = branch; const branchTypes = getBranchUpgradeTypes(branch); for (const type of branchTypes) presentTypes.add(type); if (!seenPrs.has(branchName)) { seenPrs.add(branchName); prCountByBase[base] = (prCountByBase[base] ?? 0) + 1; } tableStats[base] ??= {}; const baseStats = tableStats[base]; baseStats[manager] ??= {}; const managerStats = baseStats[manager]; for (const type of branchTypes) { const key = `${branchName}:${manager}:${type}`; if (seenTableKeys.has(key)) continue; seenTableKeys.add(key); managerStats[type] ??= 0; managerStats[type]++; } if (branch.isVulnerabilityAlert) { securityGroups[branchName] ??= []; securityGroups[branchName].push(branch); } } return { prCountByBase, tableStats, presentCategories: presentTypes, securityGroups, prCount: seenPrs.size }; } function getCategoryColumns(presentTypes) { const cols = SUMMARY_CATEGORY_PRIORITY_ORDER.filter((t) => presentTypes.has(t)); for (const t of presentTypes) if (t !== "other" && !cols.includes(t)) cols.push(t); if (presentTypes.has("other")) cols.push("other"); return cols; } function categoriesToColumnHeaders(cols) { /* v8 ignore next -- branches always have at least one upgrade, so cols is never empty in practice */ if (cols.length === 0) return ""; return ` | ${cols.join(" | ")}`; } function renderRowCounts(typeColumns, typeCounts) { return categoriesToColumnHeaders(typeColumns.map((t) => `${typeCounts[t] ?? 0}`)); } function sumCategoryCounts(rows, categoryColumns) { const sum = {}; for (const categoryCounts of rows) for (const category of categoryColumns) sum[category] = (sum[category] ?? 0) + (categoryCounts[category] ?? 0); return sum; } function getExpectedPrListSummary(config, branches) { logger.debug("getExpectedPrListSummary()"); logger.trace({ config }); let prDesc = `\n### What to Expect\n\n`; if (!branches.length) return `${prDesc}It looks like your repository dependencies are already up-to-date and no Pull Requests will be necessary right away.\n`; const stats = collectBranchStats(branches); const sortedBases = sortBaseBranches(Object.keys(stats.prCountByBase)); const hasMultipleBaseBranches = sortedBases.length > 1; const prHourlyLimit = coerceNumber(config.prHourlyLimit); const commitHourlyLimit = coerceNumber(config.commitHourlyLimit); if (hasMultipleBaseBranches) { let total = 0; const parts = sortedBases.map((base) => { const count = stats.prCountByBase[base]; total += count; const label = base ? `the \`${base}\` branch` : "the default branch"; return `${count} Pull Request${count > 1 ? "s" : ""} to ${label}`; }); const hourlyLimitsNotice = determineHourlyLimitsNotice(prHourlyLimit, commitHourlyLimit, total); prDesc += `With your current configuration, Renovate will create ${parts.join(" and ")}${hourlyLimitsNotice}:\n\n`; } else { const hourlyLimitsNotice = determineHourlyLimitsNotice(prHourlyLimit, commitHourlyLimit, stats.prCount); prDesc += `With your current configuration, Renovate will create ${stats.prCount} Pull Request${stats.prCount > 1 ? "s" : ""}${hourlyLimitsNotice}:\n\n`; } const categoryColumns = getCategoryColumns(stats.presentCategories); if (hasMultipleBaseBranches) { prDesc += `| Branch | Manager${categoriesToColumnHeaders(categoryColumns)} |\n`; prDesc += `| --- | ---${categoryColumns.map(() => " | ---").join("")} |\n`; for (const base of sortedBases) { const label = base || "$default"; for (const [manager, typeCounts] of Object.entries(stats.tableStats[base])) prDesc += `| ${label} | ${manager}${renderRowCounts(categoryColumns, typeCounts)} |\n`; } const sumRow = sumCategoryCounts(Object.values(stats.tableStats).flatMap((b) => Object.values(b)), categoryColumns); prDesc += `| **Total** | ${renderRowCounts(categoryColumns, sumRow)} |\n`; } else { prDesc += `| Manager${categoriesToColumnHeaders(categoryColumns)} |\n`; prDesc += `| ---${categoryColumns.map(() => " | ---").join("")} |\n`; for (const [manager, typeCounts] of Object.entries(stats.tableStats[sortedBases[0]])) prDesc += `| ${manager}${renderRowCounts(categoryColumns, typeCounts)} |\n`; const sumRow = sumCategoryCounts(Object.values(stats.tableStats[sortedBases[0]]), categoryColumns); prDesc += `| **Total**${renderRowCounts(categoryColumns, sumRow)} |\n`; } prDesc += `\n<small>Note that a single PR can update multiple files and/or managers, so the above rows may not align with the number of PRs being listed above.</small>\n`; if (Object.keys(stats.securityGroups).length) { prDesc += `\n**Security updates**:\n\n`; for (const groupBranches of Object.values(stats.securityGroups)) prDesc += describeSecurityGroup(groupBranches); } if (commitHourlyLimit > 0 && commitHourlyLimit < 5 && commitHourlyLimit < stats.prCount) prDesc += emojify(`\n\n:children_crossing: Branch creation and rebasing will be limited to maximum ${commitHourlyLimit} per hour, so it doesn't swamp any CI resources or overwhelm the project. See [docs for \`commitHourlyLimit\`](${GlobalConfig.get("productLinks").documentation}configuration-options/#commithourlylimit) for details.\n\n`); else if (prHourlyLimit > 0 && prHourlyLimit < 5 && prHourlyLimit < stats.prCount) prDesc += emojify(`\n\n:children_crossing: PR creation will be limited to maximum ${prHourlyLimit} per hour, so it doesn't swamp any CI resources or overwhelm the project. See [docs for \`prHourlyLimit\`](${GlobalConfig.get("productLinks").documentation}configuration-options/#prhourlylimit) for details.\n\n`); return prDesc; } function determineHourlyLimitsNotice(prHourlyLimit, commitHourlyLimit, prCount) { if (commitHourlyLimit === 0 && prHourlyLimit === 0) return " (with no configured maximum of PRs per hour)"; if (commitHourlyLimit > 0 && commitHourlyLimit < 5 && commitHourlyLimit < prCount) return emojify(` (at a maximum of ${commitHourlyLimit} PR${commitHourlyLimit > 1 ? "s" : ""}/rebase${commitHourlyLimit > 1 ? "s" : ""} per hour)`); if (prHourlyLimit > 0 && prHourlyLimit < 5 && prHourlyLimit < prCount) return emojify(` (at a maximum of ${prHourlyLimit} PR${prHourlyLimit > 1 ? "s" : ""} per hour)`); return ""; } //#endregion export { getExpectedPrList, getExpectedPrListSummary }; //# sourceMappingURL=pr-list.js.map