Merge PR
Overview
Merge a prepared PR via deterministic squash merge (--match-head-commit + explicit co-author trailer), then clean up the worktree after success.
Inputs
- Ask for PR number or URL.
- If missing, use
.local/prep.envfrom the worktree if present. - If ambiguous, ask.
Safety
- Use
gh pr merge --squashas the only path tomain. - Do not run
git pushat all during merge. - Do not use
gh pr merge --autofor maintainer landings. - Do not run gateway stop commands. Do not kill processes. Do not touch port 18792.
Execution Rule
- Execute the workflow. Do not stop after printing the TODO checklist.
- If delegating, require the delegate to run commands and capture outputs.
Known Footguns
- If you see "fatal: not a git repository", you are in the wrong directory. Move to the repo root and retry.
- Read
.local/review.md,.local/prep.md, and.local/prep.envin the worktree. Do not skip. - Always merge with
--match-head-commit "$PREP_HEAD_SHA"to prevent racing stale or changed heads. - Clean up
.worktrees/pr-<PR>only after confirmedMERGED.
Completion Criteria
- Ensure
gh pr mergesucceeds. - Ensure PR state is
MERGED, neverCLOSED. - Record the merge SHA.
- Leave a PR comment with merge SHA and prepared head SHA, and capture the comment URL.
- Run cleanup only after merge success.
First: Create a TODO Checklist
Create a checklist of all merge steps, print it, then continue and execute the commands.
Setup: Use a Worktree
Use an isolated worktree for all merge work.
sh1repo_root=$(git rev-parse --show-toplevel) 2cd "$repo_root" 3gh auth status 4 5WORKTREE_DIR=".worktrees/pr-<PR>" 6cd "$WORKTREE_DIR"
Run all commands inside the worktree directory.
Load Local Artifacts (Mandatory)
Expect these files from earlier steps:
.local/review.mdfrom/review-pr.local/prep.mdfrom/prepare-pr.local/prep.envfrom/prepare-pr
sh1ls -la .local || true 2 3for required in .local/review.md .local/prep.md .local/prep.env; do 4 if [ ! -f "$required" ]; then 5 echo "Missing $required. Stop and run /review-pr then /prepare-pr." 6 exit 1 7 fi 8done 9 10sed -n '1,120p' .local/review.md 11sed -n '1,120p' .local/prep.md 12source .local/prep.env
Steps
- Identify PR meta and verify prepared SHA still matches
sh1pr_meta_json=$(gh pr view <PR> --json number,title,state,isDraft,author,headRefName,headRefOid,baseRefName,headRepository,body) 2printf '%s\n' "$pr_meta_json" | jq '{number,title,state,isDraft,author:.author.login,head:.headRefName,headSha:.headRefOid,base:.baseRefName,headRepo:.headRepository.nameWithOwner,body}' 3pr_title=$(printf '%s\n' "$pr_meta_json" | jq -r .title) 4pr_number=$(printf '%s\n' "$pr_meta_json" | jq -r .number) 5pr_head_sha=$(printf '%s\n' "$pr_meta_json" | jq -r .headRefOid) 6contrib=$(printf '%s\n' "$pr_meta_json" | jq -r .author.login) 7is_draft=$(printf '%s\n' "$pr_meta_json" | jq -r .isDraft) 8 9if [ "$is_draft" = "true" ]; then 10 echo "ERROR: PR is draft. Stop and run /prepare-pr after draft is cleared." 11 exit 1 12fi 13 14if [ "$pr_head_sha" != "$PREP_HEAD_SHA" ]; then 15 echo "ERROR: PR head changed after /prepare-pr (expected $PREP_HEAD_SHA, got $pr_head_sha). Re-run /prepare-pr." 16 exit 1 17fi
- Run sanity checks
Stop if any are true:
- PR is a draft.
- Required checks are failing.
- Branch is behind main.
If checks are pending, wait for completion before merging. Do not use --auto.
If no required checks are configured, continue.
sh1gh pr checks <PR> --required --watch --fail-fast || true 2checks_json=$(gh pr checks <PR> --required --json name,bucket,state 2>/tmp/gh-checks.err || true) 3if [ -z "$checks_json" ]; then 4 checks_json='[]' 5fi 6required_count=$(printf '%s\n' "$checks_json" | jq 'length') 7if [ "$required_count" -eq 0 ]; then 8 echo "No required checks configured for this PR." 9fi 10printf '%s\n' "$checks_json" | jq -r '.[] | "\(.bucket)\t\(.name)\t\(.state)"' 11 12failed_required=$(printf '%s\n' "$checks_json" | jq '[.[] | select(.bucket=="fail")] | length') 13pending_required=$(printf '%s\n' "$checks_json" | jq '[.[] | select(.bucket=="pending")] | length') 14if [ "$failed_required" -gt 0 ]; then 15 echo "Required checks are failing, run /prepare-pr." 16 exit 1 17fi 18if [ "$pending_required" -gt 0 ]; then 19 echo "Required checks are still pending, retry /merge-pr when green." 20 exit 1 21fi 22 23git fetch origin main 24git fetch origin pull/<PR>/head:pr-<PR> --force 25git merge-base --is-ancestor origin/main pr-<PR> || (echo "PR branch is behind main, run /prepare-pr" && exit 1)
If anything is failing or behind, stop and say to run /prepare-pr.
- Merge PR with explicit attribution metadata
sh1reviewer=$(gh api user --jq .login) 2reviewer_id=$(gh api user --jq .id) 3coauthor_email=${COAUTHOR_EMAIL:-"$contrib@users.noreply.github.com"} 4if [ -z "$coauthor_email" ] || [ "$coauthor_email" = "null" ]; then 5 contrib_id=$(gh api users/$contrib --jq .id) 6 coauthor_email="${contrib_id}+${contrib}@users.noreply.github.com" 7fi 8 9gh_email=$(gh api user --jq '.email // ""' || true) 10git_email=$(git config user.email || true) 11mapfile -t reviewer_email_candidates < <( 12 printf '%s\n' \ 13 "$gh_email" \ 14 "$git_email" \ 15 "${reviewer_id}+${reviewer}@users.noreply.github.com" \ 16 "${reviewer}@users.noreply.github.com" | awk 'NF && !seen[$0]++' 17) 18[ "${#reviewer_email_candidates[@]}" -gt 0 ] || { echo "ERROR: could not resolve reviewer author email"; exit 1; } 19reviewer_email="${reviewer_email_candidates[0]}" 20 21cat > .local/merge-body.txt <<EOF 22Merged via /review-pr -> /prepare-pr -> /merge-pr. 23 24Prepared head SHA: $PREP_HEAD_SHA 25Co-authored-by: $contrib <$coauthor_email> 26Co-authored-by: $reviewer <$reviewer_email> 27Reviewed-by: @$reviewer 28EOF 29 30run_merge() { 31 local email="$1" 32 local stderr_file 33 stderr_file=$(mktemp) 34 if gh pr merge <PR> \ 35 --squash \ 36 --delete-branch \ 37 --match-head-commit "$PREP_HEAD_SHA" \ 38 --author-email "$email" \ 39 --subject "$pr_title (#$pr_number)" \ 40 --body-file .local/merge-body.txt \ 41 2> >(tee "$stderr_file" >&2) 42 then 43 rm -f "$stderr_file" 44 return 0 45 fi 46 merge_err=$(cat "$stderr_file") 47 rm -f "$stderr_file" 48 return 1 49} 50 51merge_err="" 52selected_merge_author_email="$reviewer_email" 53if ! run_merge "$selected_merge_author_email"; then 54 if printf '%s\n' "$merge_err" | rg -qi 'author.?email|email.*associated|associated.*email|invalid.*email' && [ "${#reviewer_email_candidates[@]}" -ge 2 ]; then 55 selected_merge_author_email="${reviewer_email_candidates[1]}" 56 echo "Retrying once with fallback author email: $selected_merge_author_email" 57 run_merge "$selected_merge_author_email" || { echo "ERROR: merge failed after fallback retry"; exit 1; } 58 else 59 echo "ERROR: merge failed" 60 exit 1 61 fi 62fi
Retry is allowed exactly once when the error is clearly author-email validation.
- Verify PR state and capture merge SHA
sh1state=$(gh pr view <PR> --json state --jq .state) 2if [ "$state" != "MERGED" ]; then 3 echo "Merge not finalized yet (state=$state), waiting up to 15 minutes..." 4 for _ in $(seq 1 90); do 5 sleep 10 6 state=$(gh pr view <PR> --json state --jq .state) 7 if [ "$state" = "MERGED" ]; then 8 break 9 fi 10 done 11fi 12 13if [ "$state" != "MERGED" ]; then 14 echo "ERROR: PR state is $state after waiting. Leave worktree and retry /merge-pr later." 15 exit 1 16fi 17 18merge_sha=$(gh pr view <PR> --json mergeCommit --jq '.mergeCommit.oid') 19if [ -z "$merge_sha" ] || [ "$merge_sha" = "null" ]; then 20 echo "ERROR: merge commit SHA missing." 21 exit 1 22fi 23 24commit_body=$(gh api repos/:owner/:repo/commits/$merge_sha --jq .commit.message) 25contrib=${contrib:-$(gh pr view <PR> --json author --jq .author.login)} 26reviewer=${reviewer:-$(gh api user --jq .login)} 27printf '%s\n' "$commit_body" | rg -q "^Co-authored-by: $contrib <" || { echo "ERROR: missing PR author co-author trailer"; exit 1; } 28printf '%s\n' "$commit_body" | rg -q "^Co-authored-by: $reviewer <" || { echo "ERROR: missing reviewer co-author trailer"; exit 1; } 29 30echo "merge_sha=$merge_sha"
- PR comment
Use a multiline heredoc with interpolation enabled.
sh1ok=0 2comment_output="" 3for _ in 1 2 3; do 4 if comment_output=$(gh pr comment <PR> -F - <<EOF 5Merged via squash. 6 7- Prepared head SHA: $PREP_HEAD_SHA 8- Merge commit: $merge_sha 9 10Thanks @$contrib! 11EOF 12); then 13 ok=1 14 break 15 fi 16 sleep 2 17done 18 19[ "$ok" -eq 1 ] || { echo "ERROR: failed to post PR comment after retries"; exit 1; } 20comment_url=$(printf '%s\n' "$comment_output" | rg -o 'https://github.com/[^ ]+/pull/[0-9]+#issuecomment-[0-9]+' -m1 || true) 21[ -n "$comment_url" ] || comment_url="unresolved" 22echo "comment_url=$comment_url"
- Clean up worktree only on success
Run cleanup only if step 4 returned MERGED.
sh1cd "$repo_root" 2git worktree remove ".worktrees/pr-<PR>" --force 3git branch -D temp/pr-<PR> 2>/dev/null || true 4git branch -D pr-<PR> 2>/dev/null || true 5git branch -D pr-<PR>-prep 2>/dev/null || true
Guardrails
- Worktree only.
- Do not close PRs.
- End in MERGED state.
- Clean up only after merge success.
- Never push to main. Use
gh pr merge --squashonly. - Do not run
git pushat all in this command.