fix(release): verify named backport provenance

This commit is contained in:
Vincent Koc
2026-07-12 13:12:30 +08:00
committed by Vincent Koc
parent 16d9ef5848
commit dc6b447c07
2 changed files with 92 additions and 38 deletions
@@ -849,19 +849,14 @@ export function canonicalMainCommitMatches(commit, candidates) {
}
const pullRequestOrigins = new Set(backportPullRequestOrigins(commit.body));
const pullRequestMatches = candidates.filter(
(candidate) =>
referencesIn(`${candidate.subject}\n${candidate.body ?? ""}`).some((number) =>
pullRequestOrigins.has(number),
) &&
authorsMatch(commit, candidate) &&
pathsOverlap(commit.changedPaths, candidate.changedPaths),
);
if (pullRequestMatches.length === 1) {
return [pullRequestMatches[0].hash];
}
if (pullRequestMatches.length > 1) {
return [];
if (pullRequestOrigins.size > 0) {
const pullRequestMatches = candidates.filter(
(candidate) =>
(candidate.pullRequests ?? []).some((number) => pullRequestOrigins.has(number)) &&
authorsMatch(commit, candidate) &&
pathsOverlap(commit.changedPaths, candidate.changedPaths),
);
return pullRequestMatches.length === 1 ? [pullRequestMatches[0].hash] : [];
}
const subject = normalizedCommitSubject(commit.subject);
@@ -1088,20 +1083,17 @@ function sourceCommits(base, target, mainRef) {
const mainCommits = canonicalMainCommits(base, mainRef);
const mainCommitsByHash = new Map(mainCommits.map((commit) => [commit.hash, commit]));
const mainCommitsBySubject = new Map();
const mainCommitsByPullRequest = new Map();
for (const commit of mainCommits) {
const subject = normalizedCommitSubject(commit.subject);
const matches = mainCommitsBySubject.get(subject) ?? [];
matches.push(commit);
mainCommitsBySubject.set(subject, matches);
for (const number of referencesIn(`${commit.subject}\n${commit.body}`)) {
const pullRequestMatches = mainCommitsByPullRequest.get(number) ?? [];
pullRequestMatches.push(commit);
mainCommitsByPullRequest.set(number, pullRequestMatches);
}
}
const canonicalMainCommitsByReleaseCommit = new Map();
const namedMainPullRequestsByReleaseCommit = new Map();
const canonicalMainHashes = new Set();
const mainAssociationCandidateHashes = new Set();
const pendingCanonicalMatches = [];
const changedPathsByCommit = new Map();
const withChangedPaths = (commit) => {
if (!changedPathsByCommit.has(commit.hash)) {
@@ -1118,22 +1110,59 @@ function sourceCommits(base, target, mainRef) {
const explicit = cherryPickOrigins(commit.body)
.map((origin) => mainCommitsByHash.get(origin))
.filter(Boolean);
if (explicit.length > 0) {
const matches = [...new Set(explicit.map((candidate) => candidate.hash))];
canonicalMainCommitsByReleaseCommit.set(commit.hash, matches);
for (const hash of matches) {
canonicalMainHashes.add(hash);
}
continue;
}
const releaseCommit = withChangedPaths(commit);
const pullRequestOrigins = backportPullRequestOrigins(commit.body);
const candidates = new Map(
[
...(mainCommitsBySubject.get(normalizedCommitSubject(commit.subject)) ?? []),
...backportPullRequestOrigins(commit.body).flatMap(
(number) => mainCommitsByPullRequest.get(number) ?? [],
),
].map((candidate) => [candidate.hash, candidate]),
(mainCommitsBySubject.get(normalizedCommitSubject(commit.subject)) ?? []).map((candidate) => [
candidate.hash,
candidate,
]),
);
if (pullRequestOrigins.length > 0) {
for (const candidate of mainCommits) {
if (!authorsMatch(releaseCommit, candidate)) {
continue;
}
const mainCandidate = withChangedPaths(candidate);
if (!pathsOverlap(releaseCommit.changedPaths, mainCandidate.changedPaths)) {
continue;
}
candidates.set(candidate.hash, candidate);
mainAssociationCandidateHashes.add(candidate.hash);
}
}
pendingCanonicalMatches.push({ candidates, commit: releaseCommit, pullRequestOrigins });
}
const candidateMainPullRequests = resolveAssociatedPullRequests(
[...mainAssociationCandidateHashes],
Number.POSITIVE_INFINITY,
);
for (const { candidates, commit, pullRequestOrigins } of pendingCanonicalMatches) {
const matches = canonicalMainCommitMatches(
commit,
[...candidates.values()].map((candidate) => ({
...withChangedPaths(candidate),
pullRequests: candidateMainPullRequests.get(candidate.hash) ?? [],
})),
);
const matches =
explicit.length > 0
? [...new Set(explicit.map((candidate) => candidate.hash))]
: canonicalMainCommitMatches(
withChangedPaths(commit),
[...candidates.values()].map(withChangedPaths),
);
canonicalMainCommitsByReleaseCommit.set(commit.hash, matches);
if (pullRequestOrigins.length > 0 && matches.length === 1) {
const associatedPullRequests = candidateMainPullRequests.get(matches[0]) ?? [];
const namedPullRequests = pullRequestOrigins.filter((number) =>
associatedPullRequests.includes(number),
);
if (namedPullRequests.length > 0) {
namedMainPullRequestsByReleaseCommit.set(commit.hash, namedPullRequests);
}
}
for (const hash of matches) {
canonicalMainHashes.add(hash);
}
@@ -1147,9 +1176,12 @@ function sourceCommits(base, target, mainRef) {
const nonRevertPullRequests = new Set();
for (const commit of activeCommits) {
const currentPullRequests = activePullRequests.get(commit.hash) ?? [];
const mainPullRequests = (canonicalMainCommitsByReleaseCommit.get(commit.hash) ?? []).flatMap(
(hash) => canonicalMainPullRequests.get(hash) ?? [],
);
const namedMainPullRequests = namedMainPullRequestsByReleaseCommit.get(commit.hash);
const mainPullRequests =
namedMainPullRequests ??
(canonicalMainCommitsByReleaseCommit.get(commit.hash) ?? []).flatMap(
(hash) => canonicalMainPullRequests.get(hash) ?? [],
);
const matchedMainCommits = canonicalMainCommitsByReleaseCommit.get(commit.hash) ?? [];
const associatedPullRequests = canonicalPullRequests(
currentPullRequests,
+25 -3
View File
@@ -57,7 +57,8 @@ describe("release-note verification", () => {
authorName: "Maintainer",
changedPaths: new Set(["src/channel.ts"]),
hash: "a".repeat(40),
subject: "fix(channel): preserve durable replies (#123)",
pullRequests: [123],
subject: "fix(channel): preserve durable replies",
};
const explicitBackport = {
authorEmail: "other@example.com",
@@ -93,11 +94,21 @@ describe("release-note verification", () => {
canonicalMainCommitMatches(pullRequestBackport, [
{
...mainCommit,
pullRequests: [],
body: "Original main PR #123.",
subject: "fix(channel): preserve durable replies",
},
]),
).toEqual([mainCommit.hash]);
).toEqual([]);
expect(
canonicalMainCommitMatches(pullRequestBackport, [
{
...mainCommit,
pullRequests: [999],
subject: "fix(channel): keep replies after renewal",
},
]),
).toEqual([]);
expect(
canonicalMainCommitMatches(
{ ...pullRequestBackport, authorEmail: "other@example.com", authorName: "Other" },
@@ -116,9 +127,20 @@ describe("release-note verification", () => {
expect(
canonicalMainCommitMatches(pullRequestBackport, [
mainCommit,
{ ...mainCommit, hash: "e".repeat(40) },
{ ...mainCommit, hash: "e".repeat(40), pullRequests: [123] },
]),
).toEqual([]);
expect(
canonicalMainCommitMatches(pullRequestBackport, [
mainCommit,
{
...mainCommit,
body: "Original main PR #123.",
hash: "f".repeat(40),
pullRequests: [],
},
]),
).toEqual([mainCommit.hash]);
expect(canonicalPullRequests([456], [123])).toEqual([123]);
});