Fresh Merge Under Test

Policy suites-test-fresh-merge: every suite job of a pull request tests the head merged onto the base branch's CURRENT tip, resolved when the suites start and printed in the run. Never GitHub's merge ref from the push, and never a stale merge on a re-run.

Why the merge ref is not enough

A pull_request run checks out refs/pull/<n>/merge at the commit the event carried. GitHub builds that merge when the head is pushed and refreshes it lazily, so the run tests the head merged onto main as main stood at the push. A re-run — a build-queue dispatch, a transient retry, the steward, a stage advance — keeps the event's commit and tests the same old merge again.

Measured on core (REST, 2026-10-05): of 18 open pull requests, 13 had a merge_commit_sha whose first parent was not main's tip, and reading every pull request again did not refresh a single one. So a green earned that way can be green against a main that has since moved. That is how "a gate fix in main does not reach open PRs" happened, and how two pull requests that were each green landed a combination nobody had compiled.

How the merge is computed

.github/actions/fresh-merge (composite, backed by .github/scripts/ci-fresh-merge.py) runs right after the checkout of the repository under test and before anything reads the tree:

main   = the base tip handed in, or `git ls-remote origin refs/heads/<base>` now
base   = git merge-base main head          (a shallow checkout deepens over git; REST is the last resort)
tree   = git merge-tree --write-tree --merge-base=<base> <main> <head>
commit = git commit-tree <tree> -p <main> -p <head>     (fixed identity, the head's commit date)
git checkout --detach <commit>

The self-test (ci-fresh-merge.py --self-test, run by core's workflow-shell job) builds throwaway repositories and proves each verdict both ways: the parents are [main tip, head]; main's NEWER change is in the tree; a shallow and a full checkout produce the identical commit; a checkout already on the fresh merge is left alone; a conflict is refused, naming the path; and, as the negative control, the merge onto the OLD main is a different tree. Swapping the parents in the script reds two of the cases.

Where it runs

Repository / lane Resolves the tip Applies the merge
core dotnet-test.yml build (its main-sha output) — as the suites START; a run held for its review is released by rerun-failed-jobs, which re-runs build but not a green precheck precheck (for the green-tree probe, on its own resolution), build, every test shard, doc-gate, platform-compat (all on the build's main), and collect-results (the refs/ci-green marker keys the BUILD's tree)
node-repo-gate.yml plan (resolve only — the shard plan is unchanged) every gate shard
node-repo-module-pack.yml select select (the build keys hash the tested tree), build-workspace, every pack and tests leg
node-repo-compile-check.yml the job itself the job itself
MeshWeaver.Plugins ci.yml admission, as the run is ADMITTED (a held run's dispatch re-runs it) — for the portal hosts and the two shared lanes; the lighter legs that do not wait for admission resolve at their own start portal-hosts-build + portal-hosts-test, modules-floor / test-repos (through the lanes' merge-main-sha), compile-check-unit, compile-check-lanes, tests-ratchet, test-drift, rn-app, e2e-static, memex-template

A caller may pass merge-main-sha to the three lanes; empty means the lane resolves the tip itself in its first job. Satellites need no change: they call the lanes at @main.

When is "now"? When the suites START. Under policy review-then-suites a run is held at the stage gate until its review is answered and then released by rerun-failed-jobs; the job that resolves the tip is one that re-runs then (core's build, Plugins' admission), so the released suites test the main of the moment they start, not the main of the push. A re-run of failed shards alone keeps the tree its build compiled — on purpose, since the shards read that build's binaries.

What it does not do (stated)