From 77f451d2698725eb9c6b83ac2e914df46987f0cd Mon Sep 17 00:00:00 2001 From: Sebastian Mendel Date: Wed, 5 Aug 2026 19:40:44 +0200 Subject: [PATCH] fix(merge-gate): re-poll UNKNOWN merge state before denying; document silent queue drops MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UNKNOWN usually means GitHub is still computing after a fresh push — the gate denied a legitimate merge on it (live). Bounded 3x3s re-poll inside the PreToolUse hook, then normal evaluation (persistent UNKNOWN still denies). merge-gate-watcher: a queued PR can silently leave the queue while reading CLEAN — verify isInMergeQueue, re-arm once via --disable-auto/--auto, then diagnose. Signed-off-by: Sebastian Mendel --- skills/git-workflow/references/merge-gate-watcher.md | 4 ++++ skills/git-workflow/scripts/merge-gate.sh | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/skills/git-workflow/references/merge-gate-watcher.md b/skills/git-workflow/references/merge-gate-watcher.md index 786e5eb..3cf90fa 100644 --- a/skills/git-workflow/references/merge-gate-watcher.md +++ b/skills/git-workflow/references/merge-gate-watcher.md @@ -150,3 +150,7 @@ the remote (`git fetch origin` then re-add the worktree tracking `origin/`) — but only while the remote ref still exists (a merged PR's branch is often auto-deleted). The discipline is cheaper than the recovery: **confirm `state == MERGED` before any destructive cleanup.** + +## A queued PR can silently leave the merge queue + +A PR queued via `gh pr merge --auto` on a merge-queue repo can drop back out with no visible event: `isInMergeQueue` flips to `false`, `mergeStateStatus` reads `CLEAN`, and nothing merges. Verify the real queue state via GraphQL (`state` / `merged` / `isInMergeQueue` / `mergeStateStatus`) — a status read that only looks at `mergeStateStatus` reports a dropped PR as merge-ready. Re-arm once (`gh pr merge --disable-auto`, then `--auto`, which forces the queue to re-evaluate); if it drops again, diagnose the queue's required contexts instead of re-arming repeatedly. diff --git a/skills/git-workflow/scripts/merge-gate.sh b/skills/git-workflow/scripts/merge-gate.sh index df57f25..bcd428d 100755 --- a/skills/git-workflow/scripts/merge-gate.sh +++ b/skills/git-workflow/scripts/merge-gate.sh @@ -36,6 +36,18 @@ fi # resolution via GraphQL (owner/repo/number parsed from the url). INFO=$(gh pr view "$PR" "${REPO_FLAG[@]}" --json mergeStateStatus,url 2>/dev/null) || exit 0 MSS=$(echo "$INFO" | jq -r '.mergeStateStatus // "null"') +# UNKNOWN usually means GitHub is still computing the state (fresh push, or a +# merge landed elsewhere moments ago). Re-poll briefly before evaluating, so a +# legitimate merge isn't denied on a transient; a persistent UNKNOWN still +# denies below (fail-closed). This is a bounded pre-check inside a PreToolUse +# hook, not a merge-driver loop — pr-status.sh --watch remains the watcher. +tries=0 +while [[ "$MSS" == "UNKNOWN" && $tries -lt 3 ]]; do + sleep 3 + INFO=$(gh pr view "$PR" "${REPO_FLAG[@]}" --json mergeStateStatus,url 2>/dev/null) || exit 0 + MSS=$(echo "$INFO" | jq -r '.mergeStateStatus // "null"') + tries=$((tries + 1)) +done URL=$(echo "$INFO" | jq -r '.url // ""') [[ "$URL" =~ github\.com/([^/]+)/([^/]+)/pull/([0-9]+) ]] || exit 0 O="${BASH_REMATCH[1]}"; RN="${BASH_REMATCH[2]}"; NUM="${BASH_REMATCH[3]}"