diff --git a/scripts/ci/pr_review_merge_scheduler.py b/scripts/ci/pr_review_merge_scheduler.py index 329c79f9..fb90b0fb 100644 --- a/scripts/ci/pr_review_merge_scheduler.py +++ b/scripts/ci/pr_review_merge_scheduler.py @@ -133,6 +133,11 @@ } REST_MERGEABLE_STATES = set(REST_MERGEABLE_STATE_MAP.values()) REST_MERGEABLE_STATE_WORKERS = 10 +DETERMINISTIC_APPROVAL_MARKERS = ( + "deterministic current-head evidence", + "deterministic fallback approval", + "did not emit a usable current-head control block", +) @dataclass @@ -955,14 +960,25 @@ def is_opencode_review(review: dict[str, Any]) -> bool: return review_author_login(review) in {"opencode-agent", "opencode-agent[bot]"} +def is_deterministic_fallback_approval(review: dict[str, Any]) -> bool: + """Return whether an old fail-open approval body is not review evidence.""" + if (review.get("state") or "").upper() != "APPROVED": + return False + body = (review.get("body") or "").lower() + return any(marker in body for marker in DETERMINISTIC_APPROVAL_MARKERS) + + def current_head_review_state(pr: dict[str, Any], state: str) -> bool: """Return whether OpenCode's latest current-head review has the target state.""" + target_state = state.upper() for review in reversed((pr.get("reviews") or {}).get("nodes") or []): if not is_opencode_review(review): continue if not review_matches_current_head(review, pr): continue - return (review.get("state") or "").upper() == state + if target_state == "APPROVED" and is_deterministic_fallback_approval(review): + return False + return (review.get("state") or "").upper() == target_state return False diff --git a/tests/test_pr_review_merge_scheduler.py b/tests/test_pr_review_merge_scheduler.py index c4de766e..2fa216a8 100644 --- a/tests/test_pr_review_merge_scheduler.py +++ b/tests/test_pr_review_merge_scheduler.py @@ -869,6 +869,28 @@ def test_review_state_and_failed_checks(): }, ) assert sched.has_current_head_approval(body_sha_match) + deterministic_fallback = make_pr( + headRefOid=exact_head, + reviews={ + "nodes": [ + { + **opencode_review("APPROVED", exact_head), + "body": ( + "OpenCode model attempts did not emit a usable current-head " + "control block, so the approval gate used deterministic " + "current-head evidence instead of model prose." + ), + } + ] + }, + ) + assert sched.is_deterministic_fallback_approval( + deterministic_fallback["reviews"]["nodes"][0] + ) + assert not sched.is_deterministic_fallback_approval( + opencode_review("CHANGES_REQUESTED", exact_head) + ) + assert not sched.has_current_head_approval(deterministic_fallback) stale_review = make_pr( reviews={ "nodes": [