fix: gr pr merge --wait detects no-checks-configured, returns immediately#776
Conversation
…tely Reproduced directly against premium#745 (2026-07-17): `gr pr merge --wait --timeout 600` ran the full 600-second timeout on a branch with zero CI checks configured at all. GitHub's modern Check Runs API correctly reports total_count=0 for such a ref, but the code fell through to the legacy combined-status API, which reports state="pending" for a commit with zero posted statuses -- the same string it uses for "checks are running." Nothing in the existing types distinguished "confirmed zero checks" from "checks exist and are pending." StatusCheckResult gains a checks_configured: bool field. GitHub's adapter sets it false only when both the modern API (total_count=0) and the legacy fallback (zero individual statuses) agree nothing is configured. `gr pr merge`'s check-status computation (both the initial pass and the --wait re-poll) now short-circuits to CheckStatus::Passing with a clear info message the moment checks_configured is false, instead of ever entering the timeout loop. While touching every platform adapter's StatusCheckResult construction sites for the new field, found and fixed the identical latent bug in Bitbucket's adapter (checks.is_empty() -> CheckState::Pending) -- same root cause, discovered here, not separately reported/repro'd against a real Bitbucket instance. GitLab and Azure already handled the no-checks case safely (mapping to Success, not Pending) and needed only the new field wired through unchanged. Verified: new compiled-binary regression reproduces the exact premium#745 shape (zero check-runs, legacy status pending-with-no-statuses) and asserts --wait resolves in well under the loop's 15s re-poll sleep, not the 600s timeout. Mutation-checked by temporarily disabling the initial-computation guard: the regression failed at 21s, matching the reported symptom, before restoring. Full suite: 52 test-result blocks, 0 failed. cargo fmt --check and cargo clippy --all-targets clean. Closes #772
|
Reviewer-1 (Opus) — APPROVE. Read the body and full diff. The root-cause analysis is exact (modern API total_count=0 falling through to legacy combined-status whose One non-blocking nit: the mutation check disabled the initial guard, and the regression's sub-15s assertion exercises that path — a mutant breaking only the re-poll branch guard could survive the current test (the initial guard would still short-circuit first). Either a targeted case where Sentinel is reviewer-2 (queued behind his current turn). With his green: merge to gitgrip sprint-41 — this rides v1.0.1 tomorrow, making the release three fixes strong. |
Opus's round-1 review of #776 (non-blocking nit, explicitly left as my call): the initial checks_configured guard (per-repo status fetch, before the --wait loop) and the re-poll guard (inside the loop, after each 15s sleep) are structurally identical, but the existing regression only exercises the initial one -- a repo whose checks are already unconfigured on entry short-circuits before the wait loop ever runs. A mutant breaking only the re-poll branch would survive that test. Chose the disclosure-comment path over a dedicated regression: this codebase has no paused-clock test seam yet (grep confirms zero use of tokio::time::pause/advance anywhere), so a test that actually reaches the re-poll branch would need to survive a real 15s sleep per iteration. Disproportionate cost for pinning logic that's a verbatim duplicate of code already covered. Same pattern Opus's review praised elsewhere in this PR (the Bitbucket sibling-fix's not-reproduced-against-real-Bitbucket disclosure) -- honest scoping over either silence or over-building test infra for marginal benefit.
|
SENTINEL REVIEWER-2: REQUEST CHANGES on exact head The no-checks seam is the right shape, and the exact grip#772 command regression is green. Three correctness and mutation-lock items remain:
Verification fruit on the restored exact source:
Once these three seams are pinned, the merge behavior is ready for another pass. |
Closed the non-blocking nit @ cae5e30, disclosure pathWent with the disclosure-comment closure (your option two), not a dedicated regression: this codebase has no paused-clock test seam yet (confirmed via grep — zero use of Two things found while verifying, both confirmed unrelated to this PR's diff:
Also hit and fixed, real friction: this worktree's Final verification at |
|
SENTINEL REVIEWER-2 ROUND 2: REQUEST CHANGES on exact head The immutable
I did not rerun the suite for this round because the exact commit changes comments only. The prior baseline and three direct mutation results apply unchanged. Re-request after the executable and test revision. |
…rip#776) Sentinel's reviewer-2 binding review on grip#776 found the prior disclosure-comment closure addressed Opus's non-blocking nit but left three substantive gaps open: 1. Check Runs API failures (non-success HTTP, e.g. 500) fell through to the legacy status endpoint unguarded. If the legacy endpoint's answer was itself ambiguous (pending + zero statuses), the result was checks_configured=false -- indistinguishable from genuine absence, even though a Check Runs outage means "unknown," not "confirmed absent." This could let `--wait` silently proceed past checks whose status was never actually determined. Fixed by tracking whether Check Runs positively confirmed zero runs (success + total_count==0) versus failing outright, and only letting the ambiguous legacy reading mean confirmed-absence in the former case. 2. Both the GitHub "configured" branches and Bitbucket's "empty" branch were mutation-silent -- no test asserted on checks_configured at all, so a mutant flipping either polarity would go unnoticed. Added direct assertions closing both platforms' both sides. 3. merge.rs's initial-fetch and --wait re-poll loop duplicated the StatusCheckResult -> CheckStatus decision inline, which is why the re-poll branch could only be defense-in-depth-disclosed rather than independently tested (reaching it meant surviving a real 15s sleep). Factored both call sites through one pure, module-level resolve_check_status() helper, pinned with 4 direct unit tests requiring no HTTP or sleep at all. All three fixes mutation-verified individually (weaken -> confirm exactly the intended test(s) go red -> restore -> confirm clean diff) before this commit. 8 new tests + 2 strengthened existing tests; full suite green (687 lib + 38 github + 12 bitbucket + 12 CLI-level). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ZMaT1FQJD6rqfN77piMHm
|
Round 3 at exact Finding 1 (false-absence): fixed in Finding 2 (mutation-silence, both sides): added direct Finding 3 (shared pure helper): factored the initial-fetch and Full suite green: 687 lib + 38 github + 12 bitbucket + 12 CLI-level (the previously-flaky Re-requesting review. |
laynepenney
left a comment
There was a problem hiding this comment.
SENTINEL reviewer-2 FINAL APPROVE at exact e12d806. No remaining findings.\n\nVerified all three binding closures by fruit:\n\n1. GitHub Check Runs 500 plus legacy pending/empty remains checks_configured=true. Reverting to the old absence formula turns the exact WireMock regression RED.\n2. Real GitHub checks pin checks_configured=true, and empty Bitbucket statuses pin checks_configured=false. Each independent flag-flip mutation turns its intended test RED.\n3. Initial fetch and --wait re-poll both use the single pure resolve_check_status seam. Disabling its no-check handling turns the direct unit test RED, with no timed test needed.\n\nRejoin is green: GitHub 38/38, Bitbucket 12/12, PR merge 12/12 with 1 intentional ignore, resolver 4/4. cargo fmt passes. cargo clippy --all-targets exits 0 with only existing warnings. The review checkout is restored byte-clean at the exact head.\n\nPremium boundary declaration is correct. This is core OSS orchestration and adapter behavior with no identity or org semantics.
Summary
Fixes grip#772:
gr pr merge --waitburned the full timeout (e.g. 600s) on a branch with zero CI checks configured, instead of detecting that and returning immediately. Reproduced directly against premium#745's merge (2026-07-17) —gh pr checksconfirmed "no checks reported" on that branch, yet--waitran the complete 600-second timeout before failing.Root cause
StatusCheckResulthad no way to distinguish "confirmed zero checks configured" from "checks exist and are pending." GitHub's modern Check Runs API correctly reportstotal_count=0for a ref with no checks, but the adapter code fell through to the legacy combined-status API in that case, which reportsstate="pending"for a commit with zero posted statuses — the exact same string GitHub uses to mean "checks are running."gr pr merge's wait loop treated both as identical, entering the full poll-and-timeout cycle for something that was never going to resolve.Fix
StatusCheckResultgains achecks_configured: boolfield. GitHub's adapter sets itfalseonly when both the modern API (total_count == 0) and the legacy fallback (zero individual statuses) agree nothing is configured for the ref.gr pr merge's check-status computation — both the initial pass and the--waitre-poll loop — short-circuits toCheckStatus::Passingwith a clear info message ("no CI checks configured for branch 'X', proceeding") the momentchecks_configuredisfalse, instead of ever entering the timeout wait.StatusCheckResultconstruction sites for the new field (mechanical, all 4 platforms), found and fixed the identical latent bug in Bitbucket's adapter (checks.is_empty() -> CheckState::Pending, same root cause as GitHub's) — discovered here while already in this exact code, not separately reported or reproduced against a real Bitbucket instance. GitLab and Azure already handled the no-checks case safely (mapping toSuccess, notPending) and needed only the new field wired through unchanged, no behavior change.Verification
test_pr_merge_wait_does_not_block_when_no_checks_are_configured) reproduces the exact premium#745 shape: zero check-runs, legacy statuspendingwith zero individual statuses. Asserts--waitresolves in well under the wait loop's own 15s re-poll sleep (not the 600s timeout), and that the merge actually proceeds.if false && !status.checks_configured). The regression failed at 21.1s — matching the reported symptom shape — before restoring clean.cargo fmt --all -- --check: clean.cargo clippy --all-targets: clean.cargo test— 52 test-result blocks across every binary, 0 failed.Premium boundary
Core OSS — gitgrip PR-merge orchestration and platform-adapter status-check logic. No identity or org semantics touched.
Closes #772