diff --git a/scripts/upstream/queue.test.ts b/scripts/upstream/queue.test.ts index 7301b484ea8a..59bc290ddf14 100644 --- a/scripts/upstream/queue.test.ts +++ b/scripts/upstream/queue.test.ts @@ -222,14 +222,19 @@ describe("chronological upstream queue", () => { const input = [integration(1), integration(2), integration(3)]; const cache = { [sha(1)]: [pr], [sha(2)]: [pr], [sha(3)]: [{ ...pr, mergedAt: null }] }; assert.deepEqual( - associatePRs(input, cache, "example/upstream", new Set([sha(2)])).map((entry) => entry.pr), + associatePRs(input, cache, "example/upstream", new Set([sha(2)]), new Set([sha(2)])).map( + (entry) => entry.pr, + ), [12, 12, null], ); assert.throws( - () => associatePRs(input, cache, "example/upstream", new Set()), + () => associatePRs(input, cache, "example/upstream", new Set(), new Set([sha(2)])), "outside target", ); - assert.throws(() => associatePRs(input, {}, "example/upstream", new Set()), "Missing GitHub"); + assert.throws( + () => associatePRs(input, {}, "example/upstream", new Set(), new Set()), + "Missing GitHub", + ); assert.throws( () => associatePRs( @@ -237,6 +242,7 @@ describe("chronological upstream queue", () => { { [sha(1)]: [pr, { ...pr, number: 13 }] }, "example/upstream", new Set([sha(2)]), + new Set([sha(2)]), ), "Ambiguous", ); @@ -256,7 +262,13 @@ describe("chronological upstream queue", () => { [sha(2)]: [pr], [sha(3)]: [{ ...pr, baseRepository: { nameWithOwner: "example/other" } }], }; - const associated = associatePRs(integrations, cache, "example/upstream", new Set([sha(2)])); + const associated = associatePRs( + integrations, + cache, + "example/upstream", + new Set([sha(2)]), + new Set([sha(2)]), + ); assert.deepEqual( associated.map((entry) => entry.pr), [12, 12, null], @@ -271,7 +283,33 @@ describe("chronological upstream queue", () => { [sha(3)], ); assert.throws( - () => associatePRs(integrations, cache, "example/upstream", new Set()), + () => associatePRs(integrations, cache, "example/upstream", new Set(), new Set([sha(2)])), + "outside target", + ); + }); + + it("treats a commit that a side branch's sync PR carried as a direct commit", () => { + const syncPR = { + number: 30, + mergedAt: "2026-01-02", + baseRefName: "feature/long-lived", + baseRepository: { nameWithOwner: "example/upstream" }, + // The sync PR merged into the side branch, so its merge commit is not on main. + mergeCommit: { oid: sha(9) }, + }; + const squashPR = { ...syncPR, number: 31, baseRefName: "main", mergeCommit: { oid: sha(2) } }; + const integrations = [integration(1), integration(2)]; + const cache = { [sha(1)]: [syncPR], [sha(2)]: [squashPR, syncPR] }; + const mainline = new Set([sha(1), sha(2)]); + assert.deepEqual( + associatePRs(integrations, cache, "example/upstream", mainline, mainline).map( + (entry) => entry.pr, + ), + [null, 31], + ); + // A PR that did merge into main still may not be split by the target. + assert.throws( + () => associatePRs(integrations, cache, "example/upstream", new Set([sha(1)]), mainline), "outside target", ); }); diff --git a/scripts/upstream/queue.ts b/scripts/upstream/queue.ts index 82df97d3d5e7..9c110200ec5d 100644 --- a/scripts/upstream/queue.ts +++ b/scripts/upstream/queue.ts @@ -331,14 +331,20 @@ export function associatePRs( associations: Associations, repository: string, chain: Set, + mainline: ReadonlySet, ): Integration[] { return integrations.map((entry) => { const prs = associations[entry.sha]; if (!prs) throw new Error(`Missing GitHub metadata for ${entry.sha}.`); // A PR can merge into an intermediate branch before its commits reach main. // The target's first-parent chain, not the PR's base branch name, establishes coverage. + // A PR whose merge commit never lands on upstream main's first-parent history only + // carried this commit into a side branch, as a "merge main into" sync PR does. const merged = prs.filter( - (pr) => pr.mergedAt !== null && pr.baseRepository.nameWithOwner === repository, + (pr) => + pr.mergedAt !== null && + pr.baseRepository.nameWithOwner === repository && + (pr.mergeCommit === null || mainline.has(pr.mergeCommit.oid)), ); if (merged.some((pr) => !pr.mergeCommit || !chain.has(pr.mergeCommit.oid))) { throw new Error( @@ -537,6 +543,7 @@ function main() { associations, state.upstreamRepository, targetChain, + new Set(upstreamChain), ); if (command === "target") { NodeFS.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`);