Skip to content

fix(#55): merge-gate hooks read PR HEAD via gh pr view, not local git HEAD#57

Merged
atlas-apex merged 1 commit into
mainfrom
fix/#55-merge-gate-pr-head
Apr 15, 2026
Merged

fix(#55): merge-gate hooks read PR HEAD via gh pr view, not local git HEAD#57
atlas-apex merged 1 commit into
mainfrom
fix/#55-merge-gate-pr-head

Conversation

@atlas-apex

Copy link
Copy Markdown
Collaborator

Summary

Closes apexstack#55. Fixes the "every merge requires gh pr checkout first" 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.sh and require-design-review-for-ui.sh both validated approval markers by comparing the marker SHA to git rev-parse HEAD. That's the local clone's HEAD — which is almost never the PR's branch HEAD when you run gh pr merge <N> from a different branch (typically main or an unrelated feature branch). So every merge during 2026-04-14 required:

gh pr checkout 241 && gh pr merge 241 --squash --delete-branch

Hit 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.sh that queries GitHub directly:

resolve_pr_head() {
  local pr_number="$1"
  local cmd_repo="$2"
  if [ -n "$cmd_repo" ]; then
    gh pr view "$pr_number" --repo "$cmd_repo" --json headRefOid --jq '.headRefOid' 2>/dev/null
  else
    gh pr view "$pr_number" --json headRefOid --jq '.headRefOid' 2>/dev/null
  fi
}

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):

CURRENT_SHA=$(resolve_pr_head "$PR_NUMBER" "$CMD_REPO")
if [ -z "$CURRENT_SHA" ]; then
  echo "WARN: Could not resolve PR #${PR_NUMBER} HEAD via gh — falling back to local HEAD..." >&2
  CURRENT_SHA=$(git rev-parse HEAD 2>/dev/null)
fi

The third merge-gate hook (block-merge-on-red-ci.sh) doesn't need the change — it uses gh pr checks <N> which is PR-scoped, not SHA-compared.

Cross-repo support (free)

resolve_pr_head takes $CMD_REPO so it works transparently for:

  • gh pr merge 42 --repo owner/repo — repo flag parsed
  • gh api repos/owner/repo/pulls/42/merge -X PUT — repo recovered from URL path by existing _lib-extract-pr.sh logic (fix: merge gate hooks don't catch gh api .../merge bypass #47)
  • gh pr merge 42 without --repo — defaults to the repo resolved by gh's working-directory context

Testing

Smoke-tested manually (bash sourcing the helper and calling with known inputs):

Test Input Output
Real PR in apexstack resolve_pr_head 53 me2resh/apexstack 2960b4dc9d803d... (the real PR #53 HEAD)
Empty PR number resolve_pr_head "" ... empty string
gh shape parser regression extract_pr_number "gh pr merge 123 --squash" 123
api shape parser regression extract_pr_number "gh api repos/me2resh/curios-dog/pulls/456/merge -X PUT" 456
merge-command detector regression is_merge_command "gh pr view 1" miss (correct)

bash -n clean on all four touched files.

Impact on ongoing sessions

Once this merges, existing hook users will notice:

  • Merges from main / any branch → work without gh pr checkout
  • On network failure, visible WARN on stderr; merge proceeds against local HEAD (may block if local is wrong — transparent error path)

No 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_context that 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

Term Definition
Local HEAD vs PR HEAD git rev-parse HEAD returns the local clone's current commit. gh pr view <N> --json headRefOid returns the commit GitHub thinks is the PR branch's tip. When you run gh pr merge from main, these diverge — and the merge targets GitHub's view, not the local one.
gh pr view for SHA resolution --json headRefOid --jq .headRefOid is the canonical way to get a PR's HEAD from GitHub without cloning, fetching, or checking out. Works for closed PRs too.
Marker SHA constraint Approval markers (<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.sh Shared bash library sourced by the three merge-gate hooks to parse PR number, repo, and (now) HEAD SHA from the raw gh pr merge or gh api .../merge command string. Not a hook itself — prefix _lib- prevents the hook-loader from firing it. Introduced in #47.
Fallback to local HEAD If gh pr view fails (network outage, auth expired, rate limit, nonexistent PR), the hook falls back to git rev-parse HEAD with 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.

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 atlas-apex left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/null on all gh calls, identical fallback pattern in both hooks
  • Testing: Pass — smoke tests cover real SHA, empty PR, regression on existing parsers, bash -n clean
  • Security: Pass — no secrets, no new attack surface; fallback is fail-safe (blocks on mismatch, never silently allows)
  • Performance: Pass — one extra gh call 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 checkout and 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

@atlas-apex atlas-apex merged commit c8c93bb into main Apr 15, 2026
4 checks passed
@atlas-apex atlas-apex deleted the fix/#55-merge-gate-pr-head branch April 15, 2026 01:21
osama-abu-baker pushed a commit to osama-abu-baker/apexyard that referenced this pull request Jun 3, 2026
…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>
mosta7il pushed a commit to mosta7il/apexyard that referenced this pull request Jun 8, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants