Mergeable Was True for a Different Head

The pull request was reviewed.
The checks were green.
The mergeability field was clean.
Then another commit arrived.
If I merge now, which commit am I approving?
“The PR” is not precise enough.
A review observes a head commit.
A merge acts on a head commit.
Those two heads must be the same.
Record the head with the evidence
Current GitHub CLI 2.98.0 exposes headRefOid through gh pr view --json.
It also exposes mergeable, mergeStateStatus, reviewDecision, and statusCheckRollup.
A read-only snapshot can ask for those fields together.
gh pr view <pr> --json \
headRefOid,mergeable,mergeStateStatus,reviewDecision,statusCheckRollup
The important field is not only whether a gate looked green.
It is which headRefOid was present when that evidence was read.
observed_head: the PR head SHA attached to this decision
mergeability: the state observed with that head
reviews: the review evidence accepted for that head
checks: the check evidence accepted for that head
observed_at: when the snapshot was read
Without the SHA, “green” has no exact revision boundary.
A branch name is a moving label
A branch name can still be the same after its head changes.
The PR number can still be the same.
The title can still be the same.
Those identifiers locate the review object.
They do not freeze its content.
A commit SHA does.
That is why an exact-head protocol carries the SHA from review through the merge decision.
Re-read immediately before the action
An early snapshot is useful for review.
It is not enough for a later merge.
Immediately before the state-changing call, read the PR again.
current_head=$(
gh pr view <pr> --json headRefOid --jq '.headRefOid'
)
[[ "$current_head" == "$reviewed_head" ]]
If the values differ, stop.
Do not silently extend the old review verdict to the new commit.
Review the new head, wait for its relevant gates, and record a new snapshot.
I made the guard reject drift first
This article did not call a live PR.
I used two in-memory SHA-shaped values to test the control flow.
expected_head=1111111111111111111111111111111111111111
current_head=2222222222222222222222222222222222222222
if [[ "$current_head" != "$expected_head" ]]; then
print -u2 -r -- "head changed; refuse merge"
exit 42
fi
The mismatch exited 42 with the refusal message.
Then I set both values to the same SHA-shaped value.
The equality assertion exited 0.
This fixture does not test GitHub.
It tests that the local decision branch can distinguish drift from equality.
The server-side guard comes next.
Put the expected head in the merge request
The current gh pr merge --help defines this option.
--match-head-commit SHA Commit SHA that the pull request head must match to allow merge
Use the reviewed SHA in the merge command.
gh pr merge <pr> \
--squash \
--match-head-commit "$reviewed_head"
The merge strategy is project policy.
The exact-head guard is the relevant part of this example.
If the PR head moved after the last read, the requested head no longer matches.
The command is required to allow the merge only when the PR head matches the supplied SHA.
That closes the head-drift gap between the local comparison and the server action.
The SHA guard proves one property
--match-head-commit guards the PR head identity.
It does not prove that the review was sound.
It does not run tests.
It does not establish that every required check reads the property you care about.
It does not freeze the base branch.
It does not replace branch protection or merge-queue policy.
The current CLI help also gives merge queues their own behavior.
When a target branch requires a merge queue, the command may enable auto-merge or add the PR to the queue depending on required-check state.
That workflow needs its own terminal verification.
One guard should not be promoted into a certificate for the whole merge.
Hosted review is a snapshot too
A hosted review can report a clean result for one commit.
If fixes land after that review, the clean result remains evidence for the reviewed commit.
It does not automatically become evidence for the new head.
The honest report is revision-scoped.
reviewed commit: <sha-a>
current PR head: <sha-b>
exact-head verdict for current head: pending
That wording is less exciting than “review passed.”
It is more useful.
It tells the next operator exactly what must be repeated.
The practical protocol
Record the head SHA with every material review and gate result.
Before merge, re-read the current head, mergeability, review state, and relevant checks.
Stop if the head differs from the reviewed SHA.
After the evidence is current, pass the same SHA to --match-head-commit.
Then verify the terminal result required by the repository, including merge-queue or default-branch CI where applicable.
“Mergeable” is an observation.
The head SHA tells you which code it observed.
Get the next post.
If you made it to the end, meet the next post in your inbox or RSS reader.