mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-31 09:41:46 +00:00
3412de84ce
* refactor(lint): enable oxlint prefer-named-capture-group at error Numbered capture groups (m[1], $1) break silently when a regex is edited and group order shifts. Named groups (?<name>...) make intent explicit and accesses (m.groups.name) resilient to reordering. This enables the built-in eslint rule prefer-named-capture-group, surfacing 321 pre-existing unnamed-group violations across ~121 files to be burned down incrementally in follow-up commits. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk * refactor(versioning): name capture groups for prefer-named-capture-group Burns down all prefer-named-capture-group violations in lib/modules/versioning/. Unused capture groups (only reachable via .test() or whole-match) become non-capturing (?:...); groups whose value is read become named (?<name>...), with accesses rewritten to match.groups!.<name> (or destructured from .groups!) instead of positional match[n]/$n so a future edit that reorders groups cannot silently break the reader. Two cases needed extra care to preserve behavior: - semver/common.ts and poetry/patterns.ts split() on a regex whose capture group is relied upon by String.prototype.split to keep the separator in the output array; the group is kept capturing and named, not converted to non-capturing. - hashicorp/convertor.ts's semverRegex is only ever embedded via .source into a named `(?<version>...)` group elsewhere and never matched directly, so its internal groups are safe to make non-capturing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk * refactor(datasource): name capture groups for prefer-named-capture-group Burns down all prefer-named-capture-group violations under lib/modules/datasource/. Same treatment as the versioning module: unused groups become non-capturing, used groups get names, and positional match[n] reads move to match.groups!.<name>. docker/ecr.ts and pypi/common.ts keep their positional destructuring (`const [, region] = ...`, `!!m &&`) working unchanged since a named capture group is still addressable by its numeric index in JavaScript — only pypi/common.ts's access was rewritten to m.groups!.owner for clarity since it read a single group directly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk * refactor(platform): name capture groups for prefer-named-capture-group Burns down all prefer-named-capture-group violations under lib/modules/platform/. Uses the repo's established idiom of `match.groups?.<name>` after an optional-chained exec()/match() (see bitbucket/index.ts hostnameWithoutApiPrefix, gerrit/utils.ts extractSourceBranch) and `$<name>` in String.replace replacement strings. Unused groups (markdown/HTML massaging regexes matched only to be stripped) become non-capturing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk * refactor(manager): name capture groups in ansible-galaxy for prefer-named-capture-group Burns down prefer-named-capture-group violations in lib/modules/manager/ansible-galaxy/. newBlockRegEx and blockLineRegEx dropped their unused outer wrapping group (only the inner key/value groups were ever read) and named the key/value groups; nameMatchRegex's remaining unused groups (protocol/user-info/port alternatives) become non-capturing since only source/hostname/depName/version are read via .groups. Accesses in collections.ts and roles.ts move from lineMatch[2]/lineMatch[3] to lineMatch.groups!.key/value. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk * refactor(manager): name capture groups for prefer-named-capture-group (batch 1) Burns down prefer-named-capture-group violations across the first alphabetical slice of lib/modules/manager/ (ansible through mix). Used groups get names with accesses rewritten to match.groups!.<name> (or $<name> in replacement strings); groups whose value is never read become non-capturing (?:...). Two String.split() call sites (git-submodules/extract.ts, meteor/extract.ts) keep the established "named group still readable positionally from the split() result array" idiom instead of restructuring the destructuring. bazel/rules/go.ts and buildpacks/extract.ts dockerRef verified byte-for-byte equivalent before/after via a quick node script since several nested groups collapsed to non-capturing at once. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk * refactor(manager): name capture groups for prefer-named-capture-group (batch 2) Burns down prefer-named-capture-group violations across the remaining alphabetical slice of lib/modules/manager/ (npm through terragrunt). Same treatment as batch 1: used groups get names with accesses moved to match.groups!.<name> or $<name> replacement strings; unused groups (wrapping alternations, quantifier repetition groups only consumed via .test(), or values never read from a match) become non-capturing. terraform/extractors/others/modules.ts and terragrunt/modules.ts share near-identical URL-parsing regexes (github/bitbucket/git/azure-devops ref matchers) — the same set of always-unused `depth=`/`&depth=` query-string groups is converted to non-capturing in both files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk * refactor(config): name capture groups for prefer-named-capture-group Burns down prefer-named-capture-group violations in lib/config/. Used groups get names with replacement strings switched from $1/$2 to $<name>; the unused separator group in schedule-migration.ts's afterBeforeRegex becomes non-capturing. presets/parse.ts keeps assigning through the already-declared `repo` variable rather than array-destructuring the exec() result, matching this file's existing style for the other named-group regexes just above it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk * refactor(util): name capture groups for prefer-named-capture-group Burns down prefer-named-capture-group violations across lib/util/ and lib/logger/. Same treatment as previous batches: named groups for values that are read (via $<name> replacement strings, .groups!.<name>, or destructured from .groups), non-capturing groups where nothing reads the captured value (e.g. semantic.ts's angular-commit .test()-only regex). www-authenticate.ts's tokenizer entries each get their own named group but keep accessing the match generically via match[1], since a positionally-indexed named group is still readable by index. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk * refactor(workers): name capture groups for prefer-named-capture-group Burns down prefer-named-capture-group violations in lib/workers/. Used groups get names with $<name> replacement strings; unused wrapper/alternation groups (e.g. semantic-commit-message.ts's optional scope/issue wrappers, release-notes.ts's date-heading and horizontal-rule matchers that are only probed with .search()/replaced wholesale) become non-capturing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk * refactor(test,tools): name capture groups for prefer-named-capture-group Finishes the prefer-named-capture-group burndown: test/, tools/, and vitest.config.mts. Groups whose value is read get names ($<name> replacement strings or .groups.<name> access); groups only used via .test()/.search() or that feed a .match(/…/g) call (whose return value is the array of whole matches, not captured groups) become non-capturing. This is the last remaining slice of pre-existing violations, so the rule (already enabled at error repo-wide) now passes with zero findings across the whole codebase. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk * fix prettier failure * refactor(lint): use a named capture group in `v8-ignore-no-count` New rule from main trips the newly enabled `prefer-named-capture-group` oxlint rule. --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
564 lines
18 KiB
TypeScript
564 lines
18 KiB
TypeScript
import { isArray, isNonEmptyString, isString } from '@sindresorhus/is';
|
|
import { DateTime } from 'luxon';
|
|
import { markdownTable } from 'markdown-table';
|
|
import semver from 'semver';
|
|
import { mergeChildConfig } from '../../../config/index.ts';
|
|
import { CONFIG_SECRETS_EXPOSED } from '../../../constants/error-messages.ts';
|
|
import { logger } from '../../../logger/index.ts';
|
|
import { coerceArray } from '../../../util/array.ts';
|
|
import { newlineRegex, regEx } from '../../../util/regex.ts';
|
|
import { sanitize } from '../../../util/sanitize.ts';
|
|
import { safeStringify } from '../../../util/stringify.ts';
|
|
import * as template from '../../../util/template/index.ts';
|
|
import type { Timestamp } from '../../../util/timestamp.ts';
|
|
import { uniq } from '../../../util/uniq.ts';
|
|
import type { BranchConfig, BranchUpgradeConfig } from '../../types.ts';
|
|
import { CommitMessage } from '../model/commit-message.ts';
|
|
|
|
function prettifyVersion(version: string): string {
|
|
if (regEx(/^\d/).test(version)) {
|
|
return `v${version}`;
|
|
}
|
|
|
|
return version;
|
|
}
|
|
|
|
function isTypesGroup(branchUpgrades: BranchUpgradeConfig[]): boolean {
|
|
return (
|
|
branchUpgrades.some(({ depName }) => depName?.startsWith('@types/')) &&
|
|
new Set(
|
|
branchUpgrades.map(({ depName }) =>
|
|
depName?.replace(regEx(/^@types\//), ''),
|
|
),
|
|
).size === 1
|
|
);
|
|
}
|
|
|
|
function sortTypesGroup(upgrades: BranchUpgradeConfig[]): void {
|
|
function isTypesUpgrade({ depName }: BranchUpgradeConfig): boolean {
|
|
return !!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: BranchUpgradeConfig): string[] | null {
|
|
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: BranchUpgradeConfig): string {
|
|
if (upgrade.semanticCommits === 'enabled' && !upgrade.commitMessagePrefix) {
|
|
logger.trace('Upgrade has semantic commits enabled');
|
|
let semanticPrefix = upgrade.semanticCommitType;
|
|
if (upgrade.semanticCommitScope) {
|
|
semanticPrefix += `(${template.compile(
|
|
upgrade.semanticCommitScope,
|
|
upgrade,
|
|
)})`;
|
|
}
|
|
upgrade.commitMessagePrefix = CommitMessage.formatPrefix(semanticPrefix!);
|
|
upgrade.toLowerCase =
|
|
regEx(/[A-Z]/).exec(upgrade.semanticCommitType!) === null &&
|
|
!upgrade.semanticCommitType!.startsWith(':');
|
|
}
|
|
|
|
// Compile a few times in case there are nested templates
|
|
upgrade.commitMessage = template.compile(
|
|
upgrade.commitMessage ?? '',
|
|
upgrade,
|
|
);
|
|
upgrade.commitMessage = template.compile(upgrade.commitMessage, upgrade);
|
|
upgrade.commitMessage = template.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(); // Trim exterior whitespace
|
|
upgrade.commitMessage = upgrade.commitMessage.replace(regEx(/\s+/g), ' '); // Trim extra whitespace inside string
|
|
upgrade.commitMessage = upgrade.commitMessage.replace(
|
|
regEx(/to vv(?<digit>\d)/),
|
|
'to v$<digit>',
|
|
);
|
|
if (upgrade.toLowerCase && upgrade.commitMessageLowerCase !== 'never') {
|
|
// We only need to lowercase the first line
|
|
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: BranchUpgradeConfig,
|
|
commitMessage: string,
|
|
): void {
|
|
if (upgrade.prTitle) {
|
|
upgrade.prTitle = template.compile(upgrade.prTitle, upgrade);
|
|
upgrade.prTitle = template.compile(upgrade.prTitle, upgrade);
|
|
upgrade.prTitle = template
|
|
.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) {
|
|
const hasVersionInTitle = !!semver.coerce(
|
|
template.compile(upgrade.commitMessageExtra ?? '', upgrade),
|
|
);
|
|
if (!hasVersionInTitle) {
|
|
upgrade.prTitle +=
|
|
upgrade.updateType === 'major' && upgrade.separateMajorMinor
|
|
? ' (major)'
|
|
: '';
|
|
upgrade.prTitle +=
|
|
upgrade.updateType === 'minor' && upgrade.separateMinorPatch
|
|
? ' (minor)'
|
|
: '';
|
|
upgrade.prTitle +=
|
|
upgrade.updateType === 'patch' && upgrade.separateMinorPatch
|
|
? ' (patch)'
|
|
: '';
|
|
}
|
|
}
|
|
}
|
|
// Compile again to allow for nested templates
|
|
upgrade.prTitle = template.compile(upgrade.prTitle, upgrade);
|
|
logger.trace(`prTitle: ${JSON.stringify(upgrade.prTitle)}`);
|
|
}
|
|
|
|
function getMinimumGroupSize(upgrades: BranchUpgradeConfig[]): number {
|
|
let minimumGroupSize = 1;
|
|
const groupSizes = new Set<number>();
|
|
|
|
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;
|
|
}
|
|
|
|
// Sorted by priority, from low to high
|
|
const semanticCommitTypeByPriority = ['chore', 'ci', 'build', 'fix', 'feat'];
|
|
|
|
export function generateBranchConfig(
|
|
upgrades: BranchUpgradeConfig[],
|
|
): BranchConfig {
|
|
let branchUpgrades = upgrades;
|
|
if (!branchUpgrades.every((upgrade) => upgrade.pendingChecks)) {
|
|
// If the branch isn't pending, then remove any upgrades within which *are*
|
|
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: BranchConfig = {
|
|
upgrades: [],
|
|
} as any;
|
|
const hasGroupName = branchUpgrades[0].groupName !== null;
|
|
logger.trace(`hasGroupName: ${hasGroupName}`);
|
|
// Use group settings only if multiple upgrades or lazy grouping is disabled
|
|
const depNames: string[] = [];
|
|
const newValue: string[] = [];
|
|
const toVersions: string[] = [];
|
|
const toValues = new Set<string>();
|
|
const depTypes = new Set<string>();
|
|
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);
|
|
}
|
|
// prettify newVersion and newMajor for printing
|
|
if (upg.newVersion) {
|
|
upg.prettyNewVersion = prettifyVersion(upg.newVersion);
|
|
}
|
|
if (upg.newMajor) {
|
|
upg.prettyNewMajor = `v${upg.newMajor}`;
|
|
}
|
|
if (upg.commitMessageExtra) {
|
|
const extra = template.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 singleUpdateGroup =
|
|
hasGroupName && !groupEligible && !branchUpgrades[0].sharedVariableName;
|
|
logger.trace(`singleUpdateGroup: ${singleUpdateGroup}`);
|
|
const useGroupSettings =
|
|
hasGroupName &&
|
|
(groupEligible ||
|
|
(singleUpdateGroup && branchUpgrades[0].groupSingleUpdates === true));
|
|
logger.trace(`useGroupSettings: ${useGroupSettings}`);
|
|
let releaseTimestamp: Timestamp;
|
|
|
|
if (depTypes.size) {
|
|
config.depTypes = Array.from(depTypes).sort();
|
|
}
|
|
|
|
for (const branchUpgrade of branchUpgrades) {
|
|
let upgrade: BranchUpgradeConfig = { ...branchUpgrade };
|
|
|
|
upgrade.depTypes = config.depTypes;
|
|
|
|
// needs to be done for each upgrade, as we reorder them below
|
|
if (newValue.length > 1 && !groupEligible) {
|
|
upgrade.commitMessageExtra = `to v${toVersions[0]}`;
|
|
}
|
|
|
|
const pendingVersionsLength = upgrade.pendingVersions?.length;
|
|
if (pendingVersionsLength) {
|
|
upgrade.displayPending = `\`${upgrade.pendingVersions!.at(-1)!}\``;
|
|
if (pendingVersionsLength > 1) {
|
|
upgrade.displayPending += ` (+${pendingVersionsLength - 1})`;
|
|
}
|
|
} else {
|
|
upgrade.displayPending = '';
|
|
}
|
|
upgrade.prettyDepType =
|
|
upgrade.prettyDepType ?? upgrade.depType ?? 'dependency';
|
|
if (useGroupSettings) {
|
|
// Now overwrite original config with group config
|
|
upgrade = mergeChildConfig(upgrade, upgrade.group);
|
|
upgrade.isGroup = true;
|
|
if (singleUpdateGroup) {
|
|
// Single update using group name instead of dependency name,
|
|
// so remove version from commit message.
|
|
delete upgrade.commitMessageExtra;
|
|
upgrade.recreateClosed = upgrade.recreateWhen !== 'never';
|
|
}
|
|
} else {
|
|
delete upgrade.groupName;
|
|
}
|
|
// Delete group config regardless of whether it was applied
|
|
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);
|
|
const upgradeStamp = DateTime.fromISO(upgrade.releaseTimestamp);
|
|
if (upgradeStamp > 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) {
|
|
// This is because we need to replace from the bottom of the file up
|
|
return a.fileReplacePosition > b.fileReplacePosition ? -1 : 1;
|
|
}
|
|
|
|
// make sure that ordering is consistent :
|
|
// items without position will be first in the list.
|
|
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;
|
|
});
|
|
}
|
|
// Now assign first upgrade's config as branch config
|
|
config = {
|
|
...config,
|
|
...config.upgrades[0],
|
|
releaseTimestamp: releaseTimestamp!,
|
|
}; // TODO: fixme (#9666)
|
|
|
|
// Enable `semanticCommits` if one of the branches has it enabled
|
|
if (
|
|
config.upgrades.some((upgrade) => upgrade.semanticCommits === 'enabled')
|
|
) {
|
|
config.semanticCommits = 'enabled';
|
|
// Calculate the highest priority `semanticCommitType`
|
|
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];
|
|
}
|
|
}
|
|
|
|
// Use templates to generate strings
|
|
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: string[], upgrade) =>
|
|
existing.concat(upgrade.prBodyColumns!),
|
|
[],
|
|
),
|
|
),
|
|
].filter(isNonEmptyString);
|
|
// combine excludeCommitPaths for multiple manager experience
|
|
const hasExcludeCommitPaths = config.upgrades.some(
|
|
(u) => u.excludeCommitPaths && u.excludeCommitPaths.length > 0,
|
|
);
|
|
if (hasExcludeCommitPaths) {
|
|
config.excludeCommitPaths = Object.keys(
|
|
config.upgrades.reduce((acc: Record<string, boolean>, upgrade) => {
|
|
if (upgrade.excludeCommitPaths) {
|
|
upgrade.excludeCommitPaths.forEach((p) => {
|
|
acc[p] = true;
|
|
});
|
|
}
|
|
|
|
return acc;
|
|
}, {}),
|
|
);
|
|
}
|
|
|
|
config.automerge = config.upgrades.every((upgrade) => upgrade.automerge);
|
|
// combine all labels
|
|
config.labels = [
|
|
...new Set(
|
|
config.upgrades
|
|
.map((upgrade) => coerceArray(upgrade.labels))
|
|
.reduce((a, b) => a.concat(b), []),
|
|
),
|
|
];
|
|
config.addLabels = [
|
|
...new Set(
|
|
config.upgrades
|
|
.map((upgrade) => coerceArray(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);
|
|
|
|
// explicit set `isLockFileMaintenance` for the branch for groups
|
|
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);
|
|
// Set skipInstalls to false if any upgrade in the branch has it false
|
|
config.skipInstalls = config.upgrades.every(
|
|
(upgrade) => upgrade.skipInstalls !== false,
|
|
);
|
|
|
|
// Artifact updating will only be skipped if every upgrade wants to skip it.
|
|
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): x is string[] => isArray(x, isString));
|
|
|
|
if (tableRows.length) {
|
|
const table: string[][] = [];
|
|
table.push(['datasource', 'package', 'from', 'to']);
|
|
|
|
const seenRows = new Set<string>();
|
|
|
|
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;
|
|
}
|