Files
Jamie Tanna 3842ec0cf2 fix(workers/repository): include tool constraints and installTools in branch commit fingerprint (#47348)
As noted in #46438, when a branch updates a tool constraint, i.e.
`constraints.golang` from `1.25` to `1.27`, any `postUpgradeTasks` with
`installTools` (and any artifact updates) in that branch would still use
the constraint from the base branch's config.

Similar to d6946be005, we should make sure
that a change to `constraints` results in a fingerprint change, so it's
clear that the branch needs reprocessing.

Although we'd excluded `postUpgradeTasks[].installTools` at that point,
we need to take them into account now, as they can affect the contents
of the branch, especially when changing `constraints`' current values.

This only includes known Containerbase tools, to avoid branch
fingerprint changes on unrelated `constraints` like `ghActionsLock`.

Co-authored-by: Claude Opus 5.5 <jamie.tanna+amp@mend.io>
Co-authored-by: Claude Sonnet 5.5 <jamie.tanna+claude-code@mend.io>
2026-10-24 14:37:03 +00:00

256 lines
8.3 KiB
TypeScript

import {
ATTR_VCS_REF_BASE_TYPE,
ATTR_VCS_REF_HEAD_NAME,
ATTR_VCS_REF_TYPE,
} from '@opentelemetry/semantic-conventions/incubating';
import { isString } from '@sindresorhus/is';
import type { RenovateConfig } from '../../../config/types.ts';
import { instrument } from '../../../instrumentation/index.ts';
import { addMeta, logger, removeMeta } from '../../../logger/index.ts';
import { hashMap } from '../../../modules/manager/index.ts';
import { scm } from '../../../modules/platform/scm.ts';
import { getCache } from '../../../util/cache/repository/index.ts';
import type { BranchCache } from '../../../util/cache/repository/types.ts';
import { isToolName } from '../../../util/exec/types.ts';
import { fingerprint } from '../../../util/fingerprint.ts';
import { setBranchNewCommit } from '../../../util/git/set-branch-commit.ts';
import { coerceObject } from '../../../util/object.ts';
import { incCountValue, setCount } from '../../global/limits.ts';
import type {
BranchConfig,
CacheFingerprintMatchResult,
UpgradeFingerprintConfig,
} from '../../types.ts';
import { processBranch } from '../update/branch/index.ts';
import { upgradeFingerprintFields } from './fingerprint-fields.ts';
import {
getCommitsHourlyCount,
getConcurrentBranchesCount,
getConcurrentPrsCount,
getPrHourlyCount,
} from './limits.ts';
export type WriteUpdateResult = 'done' | 'automerged';
export function generateCommitFingerprintConfig(
branch: BranchConfig,
): UpgradeFingerprintConfig[] {
const res = branch.upgrades.map((upgrade) => {
const filteredUpgrade = {} as UpgradeFingerprintConfig;
for (const field of upgradeFingerprintFields) {
// TS cannot narrow the type here
filteredUpgrade[field] = upgrade[field]!;
}
// only tools' constraints, as others (i.e. the default `ghActionsLock`) are set for every upgrade
const toolConstraints = Object.entries(
coerceObject(upgrade.constraints),
).filter(([name]) => isToolName(name));
if (toolConstraints.length) {
filteredUpgrade.constraints = Object.fromEntries(toolConstraints);
}
if (upgrade.postUpgradeTasks) {
filteredUpgrade.postUpgradeTasks = {
commands: upgrade.postUpgradeTasks.commands,
fileFilters: upgrade.postUpgradeTasks.fileFilters,
installTools: upgrade.postUpgradeTasks.installTools,
};
}
return filteredUpgrade;
});
return res;
}
export function compareCacheFingerprint(
branchState: BranchCache,
commitFingerprint: string,
): CacheFingerprintMatchResult {
if (!branchState.commitFingerprint) {
logger.trace('branch.isUpToDate(): no fingerprint');
return 'no-fingerprint';
}
if (commitFingerprint !== branchState.commitFingerprint) {
logger.debug('branch.isUpToDate(): needs recalculation');
return 'no-match';
}
logger.debug('branch.isUpToDate(): using cached result "true"');
return 'matched';
}
export async function syncBranchState(
branchName: string,
baseBranch: string,
): Promise<BranchCache> {
logger.debug('syncBranchState()');
const branchSha = await scm.getBranchCommit(branchName);
const baseBranchSha = await scm.getBranchCommit(baseBranch);
const cache = getCache();
cache.branches ??= [];
const { branches: cachedBranches } = cache;
let branchState = cachedBranches.find((br) => br.branchName === branchName);
if (!branchState) {
logger.debug(
'syncBranchState(): Branch cache not found, creating minimal branchState',
);
// create a minimal branch state
branchState = {
branchName,
sha: branchSha,
baseBranch,
baseBranchSha,
} as BranchCache;
cachedBranches.push(branchState);
}
// if base branch name has changed invalidate cached isModified state
if (baseBranch !== branchState.baseBranch) {
logger.debug('syncBranchState(): update baseBranch name');
branchState.baseBranch = baseBranch;
delete branchState.isModified;
branchState.pristine = false;
}
// if base branch sha has changed invalidate cached isBehindBase state
if (baseBranchSha !== branchState.baseBranchSha) {
logger.debug('syncBranchState(): update baseBranchSha');
delete branchState.isBehindBase;
delete branchState.isConflicted;
// update cached branchSha
branchState.baseBranchSha = baseBranchSha;
branchState.pristine = false;
}
// if branch sha has changed invalidate all cached states
if (branchSha !== branchState.sha) {
logger.debug('syncBranchState(): update branchSha');
delete branchState.isBehindBase;
delete branchState.isConflicted;
delete branchState.isModified;
delete branchState.commitFingerprint;
// Update commit timestamp when SHA changes
const commitDate = await scm.getBranchUpdateDate(branchName);
if (commitDate) {
branchState.commitTimestamp = commitDate.toISO()!;
}
// update cached branchSha
branchState.sha = branchSha;
branchState.pristine = false;
}
return branchState;
}
export async function writeUpdates(
config: RenovateConfig,
allBranches: BranchConfig[],
): Promise<WriteUpdateResult> {
const branches = allBranches;
logger.debug(
`Processing ${branches.length} branch${
branches.length === 1 ? '' : 'es'
}: ${branches
.map((b) => b.branchName)
.sort()
.join(', ')}`,
);
// vulnerability alerts are counted separately so that they get their own limit budget
const vulnerabilityBranches = branches.filter((b) => b.isVulnerabilityAlert);
const otherBranches = branches.filter((b) => !b.isVulnerabilityAlert);
setCount('ConcurrentPRs', await getConcurrentPrsCount(config, otherBranches));
setCount(
'VulnerabilityConcurrentPRs',
await getConcurrentPrsCount(config, vulnerabilityBranches),
);
setCount('Branches', await getConcurrentBranchesCount(otherBranches));
setCount(
'VulnerabilityBranches',
await getConcurrentBranchesCount(vulnerabilityBranches),
);
const prsThisHourCount = await getPrHourlyCount(config);
setCount('HourlyPRs', prsThisHourCount);
const commitsThisHourCount = await getCommitsHourlyCount(branches);
setCount('HourlyCommits', commitsThisHourCount);
for (const branch of branches) {
const { baseBranch, branchName } = branch;
const res = await instrument(
branchName,
async () => {
const meta: Record<string, string> = { branch: branchName };
if (config.baseBranchPatterns?.length && baseBranch) {
meta.baseBranch = baseBranch;
}
addMeta(meta);
const branchExisted = await scm.branchExists(branchName);
const branchState = await syncBranchState(branchName, baseBranch);
const managers = [
...new Set(
branch.upgrades
.map((upgrade) => hashMap.get(upgrade.manager) ?? upgrade.manager)
.filter(isString),
),
].sort();
const commitFingerprint = fingerprint({
commitFingerprintConfig: generateCommitFingerprintConfig(branch),
managers,
});
branch.cacheFingerprintMatch = compareCacheFingerprint(
branchState,
commitFingerprint,
);
const res = await processBranch(branch);
branch.prBlockedBy = res?.prBlockedBy;
branch.prNo = res?.prNo;
branch.result = res?.result;
branch.commitFingerprint = res?.updatesVerified
? commitFingerprint
: branchState.commitFingerprint;
if (res?.commitSha) {
// Get the commit timestamp for the new commit
const commitDate = await scm.getBranchUpdateDate(branchName);
setBranchNewCommit(branchName, baseBranch, res.commitSha, commitDate);
}
if (
branch.result === 'automerged' &&
branch.automergeType !== 'pr-comment'
) {
// Stop processing other branches because base branch has been changed
return 'automerged';
}
if (!branchExisted && (await scm.branchExists(branch.branchName))) {
incCountValue(
branch.isVulnerabilityAlert ? 'VulnerabilityBranches' : 'Branches',
);
}
},
{
attributes: {
[ATTR_VCS_REF_TYPE]: 'branch',
[ATTR_VCS_REF_BASE_TYPE]: 'branch',
[ATTR_VCS_REF_HEAD_NAME]: branchName,
},
},
);
if (res !== undefined) {
return res;
}
}
removeMeta(['branch', 'baseBranch']);
return 'done';
}