renovate
Version:
Automated dependency updates. Flexible so you don't need to be.
286 lines (285 loc) • 13.8 kB
JavaScript
import { CONFIG_SECRETS_EXPOSED } from "../../../constants/error-messages.js";
import { newlineRegex, regEx } from "../../../util/regex.js";
import { sanitize } from "../../../util/sanitize.js";
import { safeStringify } from "../../../util/stringify.js";
import { logger } from "../../../logger/index.js";
import { mergeChildConfig } from "../../../config/utils.js";
import { compile } from "../../../util/template/index.js";
import { uniq } from "../../../util/uniq.js";
import "../../../config/index.js";
import { CommitMessage } from "../model/commit-message.js";
import { isArray, isNonEmptyString, isString } from "@sindresorhus/is";
import { DateTime } from "luxon";
import semver from "semver";
import { markdownTable } from "markdown-table";
//#region lib/workers/repository/updates/generate.ts
function prettifyVersion(version) {
if (regEx(/^\d/).test(version)) return `v${version}`;
return version;
}
function isTypesGroup(branchUpgrades) {
return branchUpgrades.some(({ depName }) => depName?.startsWith("@types/")) && new Set(branchUpgrades.map(({ depName }) => depName?.replace(/^@types\//, ""))).size === 1;
}
function sortTypesGroup(upgrades) {
const isTypesUpgrade = ({ depName }) => !!depName?.startsWith("@types/");
const regularUpgrades = upgrades.filter((upgrade) => !isTypesUpgrade(upgrade));
const typesUpgrades = upgrades.filter(isTypesUpgrade);
upgrades.splice(0, upgrades.length);
upgrades.push(...regularUpgrades, ...typesUpgrades);
}
function getTableValues(upgrade) {
if (!upgrade.commitBodyTable) return null;
const { datasource, packageName, depName, currentVersion, newVersion } = upgrade;
const name = packageName ?? depName;
if (datasource && name && currentVersion && newVersion) return [
datasource,
name,
currentVersion,
newVersion
];
logger.trace({
datasource,
packageName,
depName,
currentVersion,
newVersion
}, "Cannot determine table values");
return null;
}
function compileCommitMessage(upgrade) {
if (upgrade.semanticCommits === "enabled" && !upgrade.commitMessagePrefix) {
logger.trace("Upgrade has semantic commits enabled");
let semanticPrefix = upgrade.semanticCommitType;
if (upgrade.semanticCommitScope) semanticPrefix += `(${compile(upgrade.semanticCommitScope, upgrade)})`;
upgrade.commitMessagePrefix = CommitMessage.formatPrefix(semanticPrefix);
upgrade.toLowerCase = regEx(/[A-Z]/).exec(upgrade.semanticCommitType) === null && !upgrade.semanticCommitType.startsWith(":");
}
upgrade.commitMessage = compile(upgrade.commitMessage ?? "", upgrade);
upgrade.commitMessage = compile(upgrade.commitMessage, upgrade);
upgrade.commitMessage = compile(upgrade.commitMessage, upgrade);
// istanbul ignore if
if (upgrade.commitMessage !== sanitize(upgrade.commitMessage)) {
logger.debug({ branchName: upgrade.branchName }, "Secrets exposed in commit message");
throw new Error(CONFIG_SECRETS_EXPOSED);
}
upgrade.commitMessage = upgrade.commitMessage.trim();
upgrade.commitMessage = upgrade.commitMessage.replace(regEx(/\s+/g), " ");
upgrade.commitMessage = upgrade.commitMessage.replace(regEx(/to vv(\d)/), "to v$1");
if (upgrade.toLowerCase && upgrade.commitMessageLowerCase !== "never") {
const splitMessage = upgrade.commitMessage.split(newlineRegex);
splitMessage[0] = splitMessage[0].toLowerCase();
upgrade.commitMessage = splitMessage.join("\n");
}
logger.trace(`commitMessage: ${JSON.stringify(upgrade.commitMessage)}`);
return upgrade.commitMessage;
}
function compilePrTitle(upgrade, commitMessage) {
if (upgrade.prTitle) {
upgrade.prTitle = compile(upgrade.prTitle, upgrade);
upgrade.prTitle = compile(upgrade.prTitle, upgrade);
upgrade.prTitle = compile(upgrade.prTitle, upgrade).trim().replace(regEx(/\s+/g), " ");
// istanbul ignore if
if (upgrade.prTitle !== sanitize(upgrade.prTitle)) {
logger.debug({ branchName: upgrade.branchName }, "Secrets were exposed in PR title");
throw new Error(CONFIG_SECRETS_EXPOSED);
}
if (upgrade.toLowerCase && upgrade.commitMessageLowerCase !== "never") upgrade.prTitle = upgrade.prTitle.toLowerCase();
} else [upgrade.prTitle] = commitMessage.split(newlineRegex);
if (!upgrade.prTitleStrict) {
upgrade.prTitle += upgrade.hasBaseBranches ? " ({{baseBranch}})" : "";
if (upgrade.isGroup) {
if (!!!semver.coerce(compile(upgrade.commitMessageExtra ?? "", upgrade))) {
upgrade.prTitle += upgrade.updateType === "major" && upgrade.separateMajorMinor ? " (major)" : "";
upgrade.prTitle += upgrade.updateType === "minor" && upgrade.separateMinorPatch ? " (minor)" : "";
upgrade.prTitle += upgrade.updateType === "patch" && upgrade.separateMinorPatch ? " (patch)" : "";
}
}
}
upgrade.prTitle = compile(upgrade.prTitle, upgrade);
logger.trace(`prTitle: ${JSON.stringify(upgrade.prTitle)}`);
}
function getMinimumGroupSize(upgrades) {
let minimumGroupSize = 1;
const groupSizes = /* @__PURE__ */ new Set();
for (const upg of upgrades) if (upg.minimumGroupSize) {
groupSizes.add(upg.minimumGroupSize);
if (minimumGroupSize < upg.minimumGroupSize) minimumGroupSize = upg.minimumGroupSize;
}
if (groupSizes.size > 1) logger.debug("Multiple minimumGroupSize values found for this branch, using highest.");
return minimumGroupSize;
}
const semanticCommitTypeByPriority = [
"chore",
"ci",
"build",
"fix",
"feat"
];
function generateBranchConfig(upgrades) {
let branchUpgrades = upgrades;
if (!branchUpgrades.every((upgrade) => upgrade.pendingChecks)) {
logger.debug({ branch: branchUpgrades[0].branchName }, "Branch is not pending, removing pending upgrades");
branchUpgrades = branchUpgrades.filter((upgrade) => !upgrade.pendingChecks);
}
logger.trace({ config: branchUpgrades }, "generateBranchConfig");
let config = { upgrades: [] };
const hasGroupName = branchUpgrades[0].groupName !== null;
logger.trace(`hasGroupName: ${hasGroupName}`);
const depNames = [];
const newValue = [];
const toVersions = [];
const toValues = /* @__PURE__ */ new Set();
const depTypes = /* @__PURE__ */ new Set();
for (const upg of branchUpgrades) {
upg.recreateClosed = upg.recreateWhen === "always";
if (upg.currentDigest) upg.currentDigestShort = upg.currentDigestShort ?? upg.currentDigest.replace("sha256:", "").substring(0, 7);
if (upg.newDigest) upg.newDigestShort = upg.newDigestShort ?? upg.newDigest.replace("sha256:", "").substring(0, 7);
if (upg.isDigest || upg.isPinDigest) {
upg.displayFrom = upg.currentDigestShort;
upg.displayTo = upg.newDigestShort;
} else if (upg.isLockfileUpdate) {
upg.displayFrom = upg.currentVersion;
upg.displayTo = upg.newVersion;
} else if (!upg.isLockFileMaintenance) {
upg.displayFrom = upg.currentValue;
upg.displayTo = upg.newValue;
}
if (upg.isLockFileMaintenance) upg.recreateClosed = upg.recreateWhen !== "never";
upg.displayFrom ??= "";
upg.displayTo ??= "";
if (!depNames.includes(upg.depName)) depNames.push(upg.depName);
if (!toVersions.includes(upg.newVersion)) toVersions.push(upg.newVersion);
toValues.add(upg.newValue);
if (upg.depType) depTypes.add(upg.depType);
if (upg.newVersion) upg.prettyNewVersion = prettifyVersion(upg.newVersion);
if (upg.newMajor) upg.prettyNewMajor = `v${upg.newMajor}`;
if (upg.commitMessageExtra) {
const extra = compile(upg.commitMessageExtra, upg);
if (!newValue.includes(extra)) newValue.push(extra);
}
}
const groupEligible = depNames.length > 1 || toVersions.length > 1 || !toVersions[0] && newValue.length > 1;
const typesGroup = depNames.length > 1 && !hasGroupName && isTypesGroup(branchUpgrades);
logger.trace(`groupEligible: ${groupEligible}`);
const useGroupSettings = hasGroupName && (groupEligible || branchUpgrades[0].groupSingleUpdates === true);
logger.trace(`useGroupSettings: ${useGroupSettings}`);
let releaseTimestamp;
if (depTypes.size) config.depTypes = Array.from(depTypes).sort();
for (const branchUpgrade of branchUpgrades) {
let upgrade = { ...branchUpgrade };
upgrade.depTypes = config.depTypes;
if (newValue.length > 1 && !groupEligible) upgrade.commitMessageExtra = `to v${toVersions[0]}`;
const pendingVersionsLength = upgrade.pendingVersions?.length;
if (pendingVersionsLength) {
upgrade.displayPending = `\`${upgrade.pendingVersions.slice(-1).pop()}\``;
if (pendingVersionsLength > 1) upgrade.displayPending += ` (+${pendingVersionsLength - 1})`;
} else upgrade.displayPending = "";
upgrade.prettyDepType = upgrade.prettyDepType ?? upgrade.depType ?? "dependency";
if (useGroupSettings) {
upgrade = mergeChildConfig(upgrade, upgrade.group);
upgrade.isGroup = true;
} else delete upgrade.groupName;
delete upgrade.group;
if (toVersions.length > 1 && toValues.size > 1 && newValue.length > 1 && !typesGroup) {
logger.trace({ toVersions });
logger.trace({ toValues });
delete upgrade.commitMessageExtra;
upgrade.recreateClosed = upgrade.recreateWhen !== "never";
} else if (newValue.length > 1 && (upgrade.isDigest || upgrade.isPinDigest)) {
logger.trace({ newValue });
delete upgrade.commitMessageExtra;
upgrade.recreateClosed = upgrade.recreateWhen !== "never";
} else if (semver.valid(toVersions[0])) upgrade.isRange = false;
config.upgrades.push(upgrade);
if (upgrade.releaseTimestamp) if (releaseTimestamp) {
const existingStamp = DateTime.fromISO(releaseTimestamp);
if (DateTime.fromISO(upgrade.releaseTimestamp) > existingStamp) releaseTimestamp = upgrade.releaseTimestamp;
} else releaseTimestamp = upgrade.releaseTimestamp;
}
if (typesGroup) {
if (config.upgrades[0].depName?.startsWith("@types/")) {
logger.debug("Found @types - reversing upgrades to use depName in PR");
sortTypesGroup(config.upgrades);
config.upgrades[0].recreateClosed = false;
config.hasTypes = true;
}
} else config.upgrades.sort((a, b) => {
if (a.fileReplacePosition && b.fileReplacePosition) return a.fileReplacePosition > b.fileReplacePosition ? -1 : 1;
if (a.fileReplacePosition) return 1;
if (b.fileReplacePosition) return -1;
if (a.depName < b.depName) return -1;
if (a.depName > b.depName) return 1;
return 0;
});
config = {
...config,
...config.upgrades[0],
releaseTimestamp
};
if (config.upgrades.some((upgrade) => upgrade.semanticCommits === "enabled")) {
config.semanticCommits = "enabled";
let highestIndex = -1;
for (const upgrade of config.upgrades) if (upgrade.semanticCommits === "enabled" && upgrade.semanticCommitType) {
const priorityIndex = semanticCommitTypeByPriority.indexOf(upgrade.semanticCommitType);
if (priorityIndex > highestIndex) highestIndex = priorityIndex;
}
if (highestIndex > -1) config.semanticCommitType = semanticCommitTypeByPriority[highestIndex];
}
const commitMessage = compileCommitMessage(config);
compilePrTitle(config, commitMessage);
config.dependencyDashboardApproval = config.upgrades.some((upgrade) => upgrade.dependencyDashboardApproval);
config.dependencyDashboardPrApproval = config.upgrades.some((upgrade) => upgrade.prCreation === "approval");
config.prBodyColumns = [...new Set(config.upgrades.reduce((existing, upgrade) => existing.concat(upgrade.prBodyColumns), []))].filter(isNonEmptyString);
if (config.upgrades.some((u) => u.excludeCommitPaths && u.excludeCommitPaths.length > 0)) config.excludeCommitPaths = Object.keys(config.upgrades.reduce((acc, upgrade) => {
if (upgrade.excludeCommitPaths) upgrade.excludeCommitPaths.forEach((p) => {
acc[p] = true;
});
return acc;
}, {}));
config.automerge = config.upgrades.every((upgrade) => upgrade.automerge);
config.labels = [...new Set(config.upgrades.map((upgrade) => upgrade.labels ?? []).reduce((a, b) => a.concat(b), []))];
config.addLabels = [...new Set(config.upgrades.map((upgrade) => upgrade.addLabels ?? []).reduce((a, b) => a.concat(b), []))];
if (config.upgrades.some((upgrade) => upgrade.updateType === "major")) config.updateType = "major";
config.isBreaking = config.upgrades.some((upgrade) => upgrade.isBreaking);
if (config.upgrades.some((upgrade) => upgrade.isLockFileMaintenance)) {
config.isLockFileMaintenance = true;
// istanbul ignore if: should never happen
if (config.upgrades.some((upgrade) => !upgrade.isLockFileMaintenance)) logger.warn("Grouping lockfile maintenance with other update types is not supported");
}
config.constraints = {};
for (const upgrade of config.upgrades) if (upgrade.constraints) config.constraints = {
...config.constraints,
...upgrade.constraints
};
config.minimumGroupSize = getMinimumGroupSize(config.upgrades);
config.skipInstalls = config.upgrades.every((upgrade) => upgrade.skipInstalls !== false);
config.skipArtifactsUpdate = config.upgrades.every((upgrade) => upgrade.skipArtifactsUpdate);
if (!config.skipArtifactsUpdate && config.upgrades.some((upgrade) => upgrade.skipArtifactsUpdate)) logger.debug({ upgrades: config.upgrades.map((upgrade) => ({
depName: upgrade.depName,
skipArtifactsUpdate: upgrade.skipArtifactsUpdate
})) }, "Mixed `skipArtifactsUpdate` values in upgrades. Artifacts will be updated.");
const tableRows = config.upgrades.map(getTableValues).filter((x) => isArray(x, isString));
if (tableRows.length) {
const table = [];
table.push([
"datasource",
"package",
"from",
"to"
]);
const seenRows = /* @__PURE__ */ new Set();
for (const row of tableRows) {
const key = safeStringify(row);
if (seenRows.has(key)) continue;
seenRows.add(key);
table.push(row);
}
config.commitMessage += `\n\n${markdownTable(table)}\n`;
}
const additionalReviewers = uniq(config.upgrades.map((upgrade) => upgrade.additionalReviewers).flat().filter(isNonEmptyString));
if (additionalReviewers.length > 0) config.additionalReviewers = additionalReviewers;
return config;
}
//#endregion
export { generateBranchConfig };
//# sourceMappingURL=generate.js.map