Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions scripts/ci/pr_review_merge_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,11 @@
"deterministic fallback approval",
"did not emit a usable current-head control block",
)
COVERAGE_REVIEW_MARKERS = (
"coverage evidence did not pass",
"coverage-evidence",
"required test/docstring evidence",
)
LAST_PUSH_APPROVAL_RESTAMP_MESSAGE = "chore: refresh head for last-push approval"


Expand Down Expand Up @@ -1193,6 +1198,33 @@ def has_current_head_changes_requested(pr: dict[str, Any]) -> bool:
return current_head_review_state(pr, "CHANGES_REQUESTED")


def current_head_coverage_change_request(pr: dict[str, Any]) -> bool:
"""Return whether the latest current-head request is only a coverage gate."""
for review in reversed((pr.get("reviews") or {}).get("nodes") or []):
if not is_opencode_review(review) or not review_matches_current_head(review, pr):
continue
if (review.get("state") or "").upper() != "CHANGES_REQUESTED":
return False
body = (review.get("body") or "").lower()
return all(marker in body for marker in COVERAGE_REVIEW_MARKERS)
return False


def coverage_evidence_state(pr: dict[str, Any]) -> str:
"""Return missing, running, complete, or failed for the latest coverage gate."""
for node in reversed(context_nodes(pr)):
name = (node.get("name") or node.get("context") or "").lower()
if name != "coverage-evidence":
continue
status = (node.get("status") or node.get("state") or "").upper()
if status in RUNNING_CHECK_STATES:
return "running"
if node.get("__typename") == "CheckRun":
return "complete" if (node.get("conclusion") or "").upper() == "SUCCESS" else "failed"
return "complete" if status == "SUCCESS" else "failed"
return "missing"


def stale_opencode_change_request_ids(pr: dict[str, Any]) -> list[int]:
"""Return dismissible automated change requests tied to previous heads."""
review_ids: list[int] = []
Expand Down Expand Up @@ -2388,6 +2420,28 @@ def request_branch_update(freshness_reason: str, *, suffix: str = "") -> Decisio
return decide("block", f"{unresolved} unresolved review thread(s)")

if has_current_head_changes_requested(pr):
coverage_ready = (
trigger_reviews
and review_dispatch_allowed
and current_head_coverage_change_request(pr)
and coverage_evidence_state(pr) == "complete"
and strix_evidence_state(pr) == "complete"
and not failed_status_checks(pr)
)
if coverage_ready:
wait_reason = repository_dispatch_wait_reason(repo, workflow)
if wait_reason:
return decide("wait", wait_reason)
dispatch_result = dispatch_opencode_review(repo, workflow, pr, dry_run=dry_run)
if dispatch_result == "already_running":
return decide(
"wait",
"current-head coverage evidence is complete, but a same-head OpenCode workflow run is already active",
)
return decide(
"review_dispatch",
"current-head OpenCode coverage blocker is cleared; same-head OpenCode re-dispatched",
)
if pr.get("autoMergeRequest"):
return finish(
disable_auto_merge_decision(
Expand Down
121 changes: 121 additions & 0 deletions tests/test_pr_review_merge_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -1239,6 +1239,69 @@ def test_review_state_and_failed_checks():
assert sched.has_current_head_approval(superseded)
assert not sched.has_current_head_changes_requested(superseded)

coverage_request = make_pr(
reviews={
"nodes": [
{
**opencode_review("CHANGES_REQUESTED", "head"),
"body": (
"OpenCode cannot approve yet because required coverage evidence did not pass. "
"The coverage-evidence gate reported that required test/docstring evidence was not proven."
),
}
]
},
statusCheckRollup={
"contexts": {
"nodes": [
strix_check(),
{
"__typename": "CheckRun",
"name": "coverage-evidence",
"status": "COMPLETED",
"conclusion": "SUCCESS",
},
]
}
},
)
assert sched.current_head_coverage_change_request(coverage_request)
assert sched.coverage_evidence_state(coverage_request) == "complete"
assert sched.coverage_evidence_state(
make_pr(
statusCheckRollup={
"contexts": {"nodes": [{"name": "coverage-evidence", "state": "PENDING"}]}
}
)
) == "running"
assert sched.coverage_evidence_state(
make_pr(
statusCheckRollup={
"contexts": {
"nodes": [
{"name": "coverage-evidence", "state": "FAILURE"},
{"name": "unrelated-check", "state": "SUCCESS"},
]
}
}
)
) == "failed"
assert sched.coverage_evidence_state(make_pr()) == "missing"
ordinary_request = make_pr(
reviews={
"nodes": [
{**opencode_review("CHANGES_REQUESTED", "head"), "body": "Fix the estimator."}
]
}
)
assert not sched.current_head_coverage_change_request(ordinary_request)
assert not sched.current_head_coverage_change_request(
make_pr(reviews={"nodes": [opencode_review("CHANGES_REQUESTED", "old")]})
)
assert not sched.current_head_coverage_change_request(
make_pr(reviews={"nodes": [opencode_review("APPROVED", "head")]})
)

stale_gate_reviews = make_pr(
reviews={
"nodes": [
Expand Down Expand Up @@ -2997,6 +3060,64 @@ def test_inspect_pr_blocks_and_waits_for_policy_states(monkeypatch):
assert inspect(make_pr(reviews={"nodes": [opencode_review("CHANGES_REQUESTED", "head")]})).reason == (
"current-head OpenCode review requested changes"
)
coverage_request = make_pr(
reviews={
"nodes": [
{
**opencode_review("CHANGES_REQUESTED", "head"),
"body": (
"OpenCode cannot approve yet because required coverage evidence did not pass. "
"The coverage-evidence gate reported that required test/docstring evidence was not proven."
),
}
]
},
statusCheckRollup={
"contexts": {
"nodes": [
strix_check(),
{
"__typename": "CheckRun",
"name": "coverage-evidence",
"status": "COMPLETED",
"conclusion": "SUCCESS",
},
]
}
},
)
dispatched = []
monkeypatch.setattr(
sched,
"dispatch_opencode_review",
lambda repo, workflow, pr, dry_run: dispatched.append(
(repo, workflow, pr["headRefOid"], dry_run)
)
or "dispatched",
)
coverage_decision = inspect(coverage_request)
assert coverage_decision.action == "review_dispatch"
assert coverage_decision.reason == (
"current-head OpenCode coverage blocker is cleared; same-head OpenCode re-dispatched"
)
assert dispatched == [("owner/repo", "OpenCode Review", "head", True)]
monkeypatch.setattr(
sched,
"repository_dispatch_wait_reason",
lambda repo, workflow: "another scheduler dispatch is active",
)
assert inspect(coverage_request).action == "wait"
monkeypatch.setattr(sched, "repository_dispatch_wait_reason", lambda repo, workflow: None)
monkeypatch.setattr(
sched,
"dispatch_opencode_review",
lambda repo, workflow, pr, dry_run: "already_running",
)
already_running = inspect(coverage_request)
assert already_running.action == "wait"
assert already_running.reason == (
"current-head coverage evidence is complete, but a same-head OpenCode workflow run is already active"
)
action_required_pr = make_pr(
statusCheckRollup={
"contexts": {
Expand Down
Loading