Summary
A stacked pull request got its branch rewritten by a GitHub-side operation, but the pull request head never followed. The branch ref and refs/pull/<n>/head then pointed at different commits, and the pull request was wedged: the merge box spun on loading stack status indefinitely, mergeable stayed UNKNOWN, and both REST routes out of the state were refused because the pull request is stacked.
Environment
- Native stack feature (stack created in the web UI, not via
gh stack).
- Private organization repository.
- Default branch protected by classic branch protection: 6 required status checks (GitHub Actions), strict (branch must be up to date). No rulesets, no merge queue, no required reviews, no CODEOWNERS.
- Stack has 2 entries, base = default branch. Position 1 was merged about four weeks earlier; position 2 stayed open. The stack still reports
"open": true and still lists the merged pull request as a member.
- Both stack members target the default branch directly (
base.ref = default branch for each).
Reproduction
- Create a 2-entry stack on the default branch.
- Merge the bottom pull request. The stack stays
open and keeps the merged pull request as position 1.
- Let the default branch advance by several commits over the following weeks.
- Have the remaining branch rewritten through GitHub — the resulting commits carry committer
GitHub, so the rewrite happened server-side, not from a local push.
Observed
After step 4 the branch ref and the pull request disagreed, and stayed that way for ~15 hours until manually unstuck:
refs/heads/<branch> = B rewritten, committer "GitHub", day 2 15:38
refs/pull/<n>/head = A pre-rewrite, committer "GitHub", day 2 01:10
GET /repos/{o}/{r}/pulls/{n} -> head.sha = A
default branch --o--o--o--o--o--o--o (advanced while the PR sat open)
\
refs/pull/<n>/head *A <- what the PR, the API and the merge box see
\
refs/heads/<branch> *B <- what the server-side rewrite actually wrote
The pull request was unmergeable from every direction:
- UI: merge box stuck on loading stack status; the spinner never resolved. No Merge button, no Update branch button, no error text.
- REST / GraphQL:
mergeable: null / UNKNOWN, mergeable_state: "unknown", mergeStateStatus: UNKNOWN — stable across repeated polls spread over many minutes. (Polling normally forces the background recomputation; here it never converged.)
statusCheckRollup reported SUCCESS — but for the stale head A.
Both REST escape hatches refused, each citing the stack:
PUT /repos/{owner}/{repo}/pulls/{n}/update-branch
403 {"message": "Updating a stacked PR's branch via this endpoint is not supported."}
PUT /repos/{owner}/{repo}/pulls/{n}/merge
403 {"message": "Merging stacked PRs via this endpoint is not supported. Use the asynchronous merge endpoint instead."}
The asynchronous endpoint that the second message points to accepted the request and then failed against the stale head:
PUT /repos/{owner}/{repo}/pulls/{n}/merge-async
{"merge_method": "squash", "merge_action": "direct_merge"}
-> {"status": "pending", "details": {"uuid": "...", "expected_head_sha": "A"}}
GET /repos/{owner}/{repo}/pulls/{n}/merge-async/{uuid}
-> {"status": "failed", "details": {"message": "6 of 6 required status checks are expected."}}
All six required checks had completed successfully on A — the very sha the endpoint echoed back as expected_head_sha. So the failure message is doubly misleading: the checks are neither missing nor pending on the head it claims to be merging.
Expected
When a stack rewrites a member branch, the pull request head follows the branch ref, exactly as an ordinary force-push does, and the merge box recovers.
Failing that, at least one route out of the state should stay open. update-branch and merge should not both refuse a stack whose only unmerged member targets the default branch directly — that combination leaves no supported way to fix a stale head, from the UI or the API.
Actual
The pull request was unmergeable from the UI and from every REST route, with no diagnostic anywhere — only an endless loading stack status spinner. Nothing in the UI indicated that the pull request was looking at a different commit than its own branch.
Workaround
Push any new commit to the branch. I merged the default branch into it with a plain local git merge + git push (no force). The synchronize event resynced the pull request head within seconds: mergeable flipped to true, mergeable_state went blocked -> unstable as the required checks re-ran on the new head, and the merge then completed normally.
One more UI symptom, after the merge
Clicking Merge on the recovered pull request also appeared to hang — the merge box kept spinning with no confirmation. The merge had in fact already succeeded: the API reported merged: true with a merge commit sha, and the commit was on the default branch. The merge box simply never updated.
Related but distinct
Both of those and this one share a theme: a stack that has had part of it merged keeps stale state somewhere, and the UI surfaces it as an unexplained spinner or an unexplained "not ready".
Summary
A stacked pull request got its branch rewritten by a GitHub-side operation, but the pull request head never followed. The branch ref and
refs/pull/<n>/headthen pointed at different commits, and the pull request was wedged: the merge box spun on loading stack status indefinitely,mergeablestayedUNKNOWN, and both REST routes out of the state were refused because the pull request is stacked.Environment
gh stack)."open": trueand still lists the merged pull request as a member.base.ref= default branch for each).Reproduction
openand keeps the merged pull request as position 1.GitHub, so the rewrite happened server-side, not from a local push.Observed
After step 4 the branch ref and the pull request disagreed, and stayed that way for ~15 hours until manually unstuck:
The pull request was unmergeable from every direction:
mergeable: null/UNKNOWN,mergeable_state: "unknown",mergeStateStatus: UNKNOWN— stable across repeated polls spread over many minutes. (Polling normally forces the background recomputation; here it never converged.)statusCheckRollupreportedSUCCESS— but for the stale headA.Both REST escape hatches refused, each citing the stack:
The asynchronous endpoint that the second message points to accepted the request and then failed against the stale head:
All six required checks had completed successfully on
A— the very sha the endpoint echoed back asexpected_head_sha. So the failure message is doubly misleading: the checks are neither missing nor pending on the head it claims to be merging.Expected
When a stack rewrites a member branch, the pull request head follows the branch ref, exactly as an ordinary force-push does, and the merge box recovers.
Failing that, at least one route out of the state should stay open.
update-branchandmergeshould not both refuse a stack whose only unmerged member targets the default branch directly — that combination leaves no supported way to fix a stale head, from the UI or the API.Actual
The pull request was unmergeable from the UI and from every REST route, with no diagnostic anywhere — only an endless loading stack status spinner. Nothing in the UI indicated that the pull request was looking at a different commit than its own branch.
Workaround
Push any new commit to the branch. I merged the default branch into it with a plain local
git merge+git push(no force). Thesynchronizeevent resynced the pull request head within seconds:mergeableflipped totrue,mergeable_statewentblocked->unstableas the required checks re-ran on the new head, and the merge then completed normally.One more UI symptom, after the merge
Clicking Merge on the recovered pull request also appeared to hang — the merge box kept spinning with no confirmation. The merge had in fact already succeeded: the API reported
merged: truewith a merge commit sha, and the commit was on the default branch. The merge box simply never updated.Related but distinct
UNKNOWN.Both of those and this one share a theme: a stack that has had part of it merged keeps stale state somewhere, and the UI surfaces it as an unexplained spinner or an unexplained "not ready".