Skip to content

Commit 6adf5c8

Browse files
johanzanderclaude
andcommitted
fix: mark_ready needs green checks, not just a clean merge
Found by running the rule against the live fleet on its first pass, which is the only reason it was caught before the PR merged. `mergeable` reports whether the branch merges cleanly and nothing else, so it reads MERGEABLE while CI is still running or has failed outright. #633 was APPROVED, MERGEABLE and had Algorithm tests and E2E still IN_PROGRESS, and the rule duly reported "gh pr ready 633 — then it is the maintainers to merge". GitHub itself disagreed: `mergeStateStatus` was BLOCKED. Flipping a red or pending PR out of draft is worse than leaving it there. `ready` is supposed to mean the maintainer can merge without checking anything else, and that claim is the only thing making the flag worth setting. So `mark_ready` now also requires every check to have concluded SUCCESS, SKIPPED or NEUTRAL. SKIPPED is green on purpose: this repo path-filters Algorithm tests and Docker build, so every backend-only PR skips them and treating that as not-green would withhold the action from almost everything. An empty rollup is green too — a PR with no checks configured has nothing failing. The deferred list mirrors the same condition, so an approved-but-pending PR with a P4 card appears in exactly one place rather than both. Live effect: #633 falls back to resume_implementation until its CI settles, which is the correct answer and the one the first version got wrong. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012LExo6fcbup75vtc9NfoAR
1 parent 496fb57 commit 6adf5c8

2 files changed

Lines changed: 92 additions & 2 deletions

File tree

backend/tests/test_backlog_rhythm.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,10 @@ def _pr(number: int, **over: object) -> dict:
364364
"reviewDecision": None,
365365
"reviews": [],
366366
"author": {"login": "johanzander"},
367+
# Defaults to green, so a test that cares about checks says so. An
368+
# empty rollup also reads as green, which is correct: a PR with no
369+
# checks configured has nothing failing.
370+
"statusCheckRollup": [{"name": "CI", "conclusion": "SUCCESS"}],
367371
}
368372
pr.update(over)
369373
return pr
@@ -645,3 +649,71 @@ def test_a_pr_with_no_card_is_reported_as_before(tmp_path: Path) -> None:
645649

646650
assert "resolve_conflict" in _actions_for(result, 614)
647651
assert result["deferred"] == []
652+
653+
654+
def test_mark_ready_needs_green_checks_not_just_a_clean_merge(
655+
tmp_path: Path,
656+
) -> None:
657+
"""`mergeable` reports only whether the branch merges cleanly, so it reads
658+
MERGEABLE while checks are still running. #633 was APPROVED and MERGEABLE
659+
with Algorithm tests and E2E in progress, and the first version of this
660+
rule duly said to flip it — which would hand the maintainer a PR marked
661+
ready whose CI had not finished."""
662+
pr = _pr(
663+
633,
664+
isDraft=True,
665+
reviews=[{"state": "APPROVED"}],
666+
statusCheckRollup=[
667+
{"name": "Fast tests", "conclusion": "SUCCESS"},
668+
{"name": "Algorithm tests", "conclusion": ""},
669+
],
670+
)
671+
actions = _actions_for(_run(tmp_path, [], [pr]), 633)
672+
assert "mark_ready" not in actions
673+
assert "resume_implementation" in actions
674+
675+
676+
def test_mark_ready_is_withheld_on_a_failing_check(tmp_path: Path) -> None:
677+
pr = _pr(
678+
634,
679+
isDraft=True,
680+
reviews=[{"state": "APPROVED"}],
681+
statusCheckRollup=[
682+
{"name": "Fast tests", "conclusion": "SUCCESS"},
683+
{"name": "E2E tests", "conclusion": "FAILURE"},
684+
],
685+
)
686+
assert "mark_ready" not in _actions_for(_run(tmp_path, [], [pr]), 634)
687+
688+
689+
def test_a_skipped_check_still_counts_as_green(tmp_path: Path) -> None:
690+
"""Path-filtered jobs correctly do not run — every backend-only PR in this
691+
repo skips Algorithm tests and Docker build, so treating SKIPPED as
692+
not-green would withhold mark_ready from almost every PR."""
693+
pr = _pr(
694+
635,
695+
isDraft=True,
696+
reviews=[{"state": "APPROVED"}],
697+
statusCheckRollup=[
698+
{"name": "Fast tests", "conclusion": "SUCCESS"},
699+
{"name": "Algorithm tests", "conclusion": "SKIPPED"},
700+
],
701+
)
702+
assert "mark_ready" in _actions_for(_run(tmp_path, [], [pr]), 635)
703+
704+
705+
def test_a_pending_approved_draft_is_not_counted_as_deferred(
706+
tmp_path: Path,
707+
) -> None:
708+
"""The deferred list mirrors the mark_ready carve-out, so a card-deferred
709+
PR that is approved-but-pending must appear in exactly one place."""
710+
pr = _pr(
711+
636,
712+
isDraft=True,
713+
reviews=[{"state": "APPROVED"}],
714+
statusCheckRollup=[{"name": "CI", "conclusion": ""}],
715+
)
716+
result = _run(tmp_path, [], [pr], pr_board=[_card(636, priority="P4")])
717+
718+
assert _actions_for(result, 636) == set()
719+
assert [d["pr"] for d in result["deferred"]] == [636]

scripts/backlog-rhythm.sh

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ if [ -n "${RHYTHM_PRS_FILE:-}" ]; then
5959
prs=$(cat "$RHYTHM_PRS_FILE")
6060
else
6161
prs=$(gh pr list --repo "$repo" --state open --limit 100 \
62-
--json number,title,isDraft,mergeable,reviewDecision,reviews,author)
62+
--json number,title,isDraft,mergeable,reviewDecision,reviews,author,statusCheckRollup)
6363
fi
6464

6565
actions=$(printf '%s' "$digest" | jq \
@@ -250,7 +250,21 @@ actions=$(printf '%s' "$digest" | jq \
250250
| ([ .reviews[]? | select(.state != "COMMENTED") ] | last | .state?) as $verdict
251251
| (.reviewDecision == "APPROVED"
252252
or ($verdict == "APPROVED" and .reviewDecision != "CHANGES_REQUESTED")) as $is_approved
253-
| (.isDraft and $is_approved and .mergeable != "CONFLICTING") as $approved_draft
253+
# CI MUST BE GREEN, not merely unconflicted. `mergeable` reports only
254+
# whether the branch merges cleanly, so it reads MERGEABLE while checks
255+
# are still running or have failed -- #633 was APPROVED and MERGEABLE
256+
# with Algorithm tests and E2E still in progress, and an earlier version
257+
# of this rule duly said to flip it. Flipping a red or pending PR ready
258+
# is worse than leaving it draft: `ready` is supposed to mean the
259+
# maintainer can merge without checking anything.
260+
#
261+
# SKIPPED counts as fine (a path-filtered job that correctly did not
262+
# run); anything still QUEUED or IN_PROGRESS is not a verdict yet.
263+
| ([ .statusCheckRollup[]?
264+
| select((.conclusion // "") != "SUCCESS" and (.conclusion // "") != "SKIPPED"
265+
and (.conclusion // "") != "NEUTRAL") ] | length == 0) as $checks_green
266+
| (.isDraft and $is_approved and $checks_green
267+
and .mergeable != "CONFLICTING") as $approved_draft
254268
| (if $approved_draft
255269
then {pr: .number, action: "mark_ready",
256270
why: "APPROVED and green, still a draft — Step 11 never ran gh pr ready",
@@ -355,6 +369,10 @@ actions=$(printf '%s' "$digest" | jq \
355369
# counted here as well.
356370
| select((.isDraft
357371
and .mergeable != "CONFLICTING"
372+
and ([ .statusCheckRollup[]?
373+
| select((.conclusion // "") != "SUCCESS"
374+
and (.conclusion // "") != "SKIPPED"
375+
and (.conclusion // "") != "NEUTRAL") ] | length == 0)
358376
and (.reviewDecision == "APPROVED"
359377
or (([ .reviews[]? | select(.state != "COMMENTED") ] | last | .state?) == "APPROVED"
360378
and .reviewDecision != "CHANGES_REQUESTED"))) | not)

0 commit comments

Comments
 (0)