fix(#55): merge-gate hooks read PR HEAD via gh pr view, not local git HEAD#57
Conversation
Previously `block-unreviewed-merge.sh` and `require-design-review-for-ui.sh` compared approval markers against `git rev-parse HEAD` (local working tree's HEAD). That rarely matched the PR's branch — usually the local clone was on main or a different feature branch — so every merge required a `gh pr checkout <N>` dance before `gh pr merge <N>`. Hit repeatedly during the 2026-04-14 merge train (6 PRs, every one needed the checkout). Fix: add resolve_pr_head() helper in _lib-extract-pr.sh that queries `gh pr view <N> --repo <owner/repo> --json headRefOid`. Both hooks now resolve the PR's real HEAD from GitHub and fall back to local HEAD with a visible warning only when the gh call fails (network, auth, rate limit). The third merge-gate hook (block-merge-on-red-ci.sh) didn't need the change — it uses `gh pr checks` which is PR-scoped, not SHA-compared. Smoke-tested: - resolve_pr_head 53 me2resh/apexstack -> real SHA - empty PR number -> empty string (safe fallback) - extract_pr_number + is_merge_command regression tests still pass Upstream issue: #55
atlas-apex
left a comment
There was a problem hiding this comment.
Code Review: PR #57
Commit: a0ffeab9b3feae5282bc1f8a31a81146d5ffc8c0
Summary
Fixes #55 — merge-gate hooks now resolve PR HEAD via gh pr view instead of local git rev-parse HEAD, eliminating the gh pr checkout <N> dance before every merge. Adds resolve_pr_head() helper to _lib-extract-pr.sh; both SHA-comparing hooks (block-unreviewed-merge.sh, require-design-review-for-ui.sh) swap to it with a local-HEAD fallback + stderr WARN on gh failure. block-merge-on-red-ci.sh correctly untouched (uses gh pr checks, not SHA comparison).
Checklist Results
- Architecture & Design: Pass — minimal, surgical change; scope correctly resisted deeper refactor
- Code Quality: Pass — empty PR short-circuit,
2>/dev/nullon all gh calls, identical fallback pattern in both hooks - Testing: Pass — smoke tests cover real SHA, empty PR, regression on existing parsers,
bash -nclean - Security: Pass — no secrets, no new attack surface; fallback is fail-safe (blocks on mismatch, never silently allows)
- Performance: Pass — one extra
ghcall per merge attempt; negligible - PR Description & Glossary: Pass — excellent glossary covering local-vs-PR HEAD, gh mechanics, marker constraint,
_lib-convention, fallback rationale - Technical Decisions (AgDR): N/A — bug fix preserving existing design; no new library/pattern/architecture
Issues Found
None blocking.
Suggestions
- Minor: WARN message is already actionable (mentions both
gh pr checkoutand re-auth). Could optionally log to a session file for post-mortem if network failures become a pattern — but overkill for now.
Verdict
APPROVED (comment-only: cannot self-approve own PR via GitHub API; Rex marker written separately at .claude/session/reviews/57-rex.approved)
CI: all 4 checks green (shellcheck, markdownlint-cli2, lychee, Verify Ticket ID).
🤖 Reviewed by Rex (Code Reviewer Agent)
📌 Reviewed commit: a0ffeab9b3feae5282bc1f8a31a81146d5ffc8c0
…me2resh#57) Previously `block-unreviewed-merge.sh` and `require-design-review-for-ui.sh` compared approval markers against `git rev-parse HEAD` (local working tree's HEAD). That rarely matched the PR's branch — usually the local clone was on main or a different feature branch — so every merge required a `gh pr checkout <N>` dance before `gh pr merge <N>`. Hit repeatedly during the 2026-04-14 merge train (6 PRs, every one needed the checkout). Fix: add resolve_pr_head() helper in _lib-extract-pr.sh that queries `gh pr view <N> --repo <owner/repo> --json headRefOid`. Both hooks now resolve the PR's real HEAD from GitHub and fall back to local HEAD with a visible warning only when the gh call fails (network, auth, rate limit). The third merge-gate hook (block-merge-on-red-ci.sh) didn't need the change — it uses `gh pr checks` which is PR-scoped, not SHA-compared. Smoke-tested: - resolve_pr_head 53 me2resh/apexstack -> real SHA - empty PR number -> empty string (safe fallback) - extract_pr_number + is_merge_command regression tests still pass Upstream issue: me2resh#55 Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
…me2resh#57) Previously `block-unreviewed-merge.sh` and `require-design-review-for-ui.sh` compared approval markers against `git rev-parse HEAD` (local working tree's HEAD). That rarely matched the PR's branch — usually the local clone was on main or a different feature branch — so every merge required a `gh pr checkout <N>` dance before `gh pr merge <N>`. Hit repeatedly during the 2026-04-14 merge train (6 PRs, every one needed the checkout). Fix: add resolve_pr_head() helper in _lib-extract-pr.sh that queries `gh pr view <N> --repo <owner/repo> --json headRefOid`. Both hooks now resolve the PR's real HEAD from GitHub and fall back to local HEAD with a visible warning only when the gh call fails (network, auth, rate limit). The third merge-gate hook (block-merge-on-red-ci.sh) didn't need the change — it uses `gh pr checks` which is PR-scoped, not SHA-compared. Smoke-tested: - resolve_pr_head 53 me2resh/apexstack -> real SHA - empty PR number -> empty string (safe fallback) - extract_pr_number + is_merge_command regression tests still pass Upstream issue: me2resh#55 Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Summary
Closes apexstack#55. Fixes the "every merge requires
gh pr checkoutfirst" papercut by making the two SHA-comparing merge-gate hooks read the PR's HEAD from GitHub instead of the local working tree.The problem
block-unreviewed-merge.shandrequire-design-review-for-ui.shboth validated approval markers by comparing the marker SHA togit rev-parse HEAD. That's the local clone's HEAD — which is almost never the PR's branch HEAD when you rungh pr merge <N>from a different branch (typicallymainor an unrelated feature branch). So every merge during 2026-04-14 required:gh pr checkout 241 && gh pr merge 241 --squash --delete-branchHit on every merge of the 6-PR train that day. Tedious, forgettable, and left stray local branches behind.
The fix
Add a
resolve_pr_head()helper to_lib-extract-pr.shthat queries GitHub directly:Both affected hooks now use this instead of
git rev-parse HEAD, with a fallback to local HEAD + a visible warning only if the gh call fails (network / auth / rate limit):The third merge-gate hook (
block-merge-on-red-ci.sh) doesn't need the change — it usesgh pr checks <N>which is PR-scoped, not SHA-compared.Cross-repo support (free)
resolve_pr_headtakes$CMD_REPOso it works transparently for:gh pr merge 42 --repo owner/repo— repo flag parsedgh api repos/owner/repo/pulls/42/merge -X PUT— repo recovered from URL path by existing_lib-extract-pr.shlogic (fix: merge gate hooks don't catch gh api .../merge bypass #47)gh pr merge 42without--repo— defaults to the repo resolved bygh's working-directory contextTesting
Smoke-tested manually (
bashsourcing the helper and calling with known inputs):resolve_pr_head 53 me2resh/apexstack2960b4dc9d803d...(the real PR #53 HEAD)resolve_pr_head "" ...extract_pr_number "gh pr merge 123 --squash"123extract_pr_number "gh api repos/me2resh/curios-dog/pulls/456/merge -X PUT"456is_merge_command "gh pr view 1"bash -nclean on all four touched files.Impact on ongoing sessions
Once this merges, existing hook users will notice:
main/ any branch → work withoutgh pr checkoutNo config change required. No workflow-gates rule change required.
Why a PR, not a deeper refactor
I considered moving ALL SHA resolution into the shared helper (a
resolve_approval_contextthat returns both PR HEAD and marker path). Decided against it — too much scope for a papercut fix. Future refactor if/when the merge gates diverge further.Upstream issue: #55
Glossary
git rev-parse HEADreturns the local clone's current commit.gh pr view <N> --json headRefOidreturns the commit GitHub thinks is the PR branch's tip. When you rungh pr mergefrommain, these diverge — and the merge targets GitHub's view, not the local one.gh pr viewfor SHA resolution--json headRefOid --jq .headRefOidis the canonical way to get a PR's HEAD from GitHub without cloning, fetching, or checking out. Works for closed PRs too.<pr>-rex.approved,<pr>-ceo.approved,<pr>-design.approved) each store a specific commit SHA. The merge gates refuse if the PR's current HEAD differs from the marker — that's how "new commits after approval invalidate it" is enforced. This PR doesn't change that; it just changes which HEAD the comparison uses._lib-extract-pr.shgh pr mergeorgh api .../mergecommand string. Not a hook itself — prefix_lib-prevents the hook-loader from firing it. Introduced in #47.gh pr viewfails (network outage, auth expired, rate limit, nonexistent PR), the hook falls back togit rev-parse HEADwith a visible warning. Worse case: the merge blocks because local HEAD is wrong, and the user sees WHY in stderr and can retry. Better than silently allowing merges.