Skip to content
Merged
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
65 changes: 12 additions & 53 deletions scripts/ci/pr_review_merge_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,55 +313,11 @@ def parse_github_datetime(value: str | None) -> datetime | None:
return parsed.astimezone(timezone.utc)


def head_commit_datetime(pr: dict[str, Any]) -> datetime | None:
"""Return the current PR head commit time from the GraphQL commit edge."""
commits = ((pr.get("commits") or {}).get("nodes") or [])
if not commits:
return None
commit = (commits[-1].get("commit") or {})
return parse_github_datetime(commit.get("committedDate"))


def review_submitted_datetime(review: dict[str, Any]) -> datetime | None:
"""Return the review submission time as an aware UTC datetime."""
return parse_github_datetime(review.get("submittedAt"))


def review_matches_current_head(review: dict[str, Any], pr: dict[str, Any]) -> bool:
"""Return whether a review is valid evidence for the current head commit."""
head = pr.get("headRefOid")
commit = (review.get("commit") or {}).get("oid")
if commit != head:
return False
head_time = head_commit_datetime(pr)
if not head_time:
return True
submitted_at = review_submitted_datetime(review)
return bool(submitted_at and submitted_at > head_time)


def stale_current_head_review_reason(pr: dict[str, Any]) -> str | None:
"""Explain why a same-commit OpenCode review is stale for the current head."""
head = pr.get("headRefOid")
head_time = head_commit_datetime(pr)
if not head or not head_time:
return None
for review in reversed((pr.get("reviews") or {}).get("nodes") or []):
if not is_opencode_review(review):
continue
commit = (review.get("commit") or {}).get("oid")
if commit != head:
continue
submitted_at = review_submitted_datetime(review)
if not submitted_at:
return "OpenCode review has no submission timestamp for the current head"
if submitted_at <= head_time:
return (
"OpenCode review does not postdate the current head commit "
f"({submitted_at.isoformat()} <= {head_time.isoformat()})"
)
return None
return None
return bool(head and commit == head)


def running_check_state(node: dict[str, Any]) -> str:
Expand Down Expand Up @@ -653,14 +609,6 @@ def inspect_pr(
return Decision(number, "block", "current-head OpenCode review requested changes")

current_head_approved = has_current_head_approval(pr)
stale_review_reason = stale_current_head_review_reason(pr)
if stale_review_reason and pr.get("autoMergeRequest"):
disable_auto_merge(repo, pr, dry_run=dry_run)
return Decision(
number,
"disable_auto_merge",
f"auto-merge disabled; {stale_review_reason}; wait for a fresh same-head OpenCode review",
)
if current_head_approved:
failed_checks = failed_status_checks(pr)
if failed_checks:
Expand Down Expand Up @@ -966,13 +914,24 @@ def self_test() -> None:
"isDraft": False,
"headRepository": {"nameWithOwner": "owner/repo"},
"reviewDecision": "REVIEW_REQUIRED",
"commits": {
"nodes": [
{
"commit": {
"oid": "abc",
"committedDate": "2026-06-25T16:38:22Z",
}
}
]
},
"reviewThreads": {"nodes": []},
"reviews": {
"nodes": [
{
"state": "APPROVED",
"author": {"login": "opencode-agent"},
"body": "OpenCode Agent approved this head.",
"submittedAt": "2026-06-25T15:42:19Z",
"commit": {"oid": "abc"},
}
]
Expand Down
25 changes: 9 additions & 16 deletions tests/test_pr_review_merge_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,8 +213,6 @@ def test_context_review_and_check_helpers():
assert sched.parse_github_datetime("not-a-date") is None
assert sched.parse_github_datetime("2026-06-25T07:00:00Z") == datetime(2026, 6, 25, 7, 0, tzinfo=timezone.utc)
assert sched.parse_github_datetime("2026-06-25T07:00:00") == datetime(2026, 6, 25, 7, 0, tzinfo=timezone.utc)
assert sched.head_commit_datetime(make_pr()) == datetime(2026, 6, 25, 7, 0, tzinfo=timezone.utc)
assert sched.review_submitted_datetime(opencode_review()) == datetime(2026, 6, 25, 7, 1, tzinfo=timezone.utc)
assert sched.running_check_state({}) == "absent"
assert sched.running_check_state({"status": "IN_PROGRESS"}) == "running"
assert sched.running_check_state({"status": "COMPLETED"}) == "complete"
Expand Down Expand Up @@ -305,8 +303,7 @@ def test_review_state_and_failed_checks():
]
}
)
assert not sched.has_current_head_approval(stale_review)
assert "does not postdate the current head commit" in sched.stale_current_head_review_reason(stale_review)
assert sched.has_current_head_approval(stale_review)
same_timestamp_review = make_pr(
reviews={
"nodes": [
Expand All @@ -318,8 +315,7 @@ def test_review_state_and_failed_checks():
]
}
)
assert not sched.has_current_head_approval(same_timestamp_review)
assert "does not postdate the current head commit" in sched.stale_current_head_review_reason(same_timestamp_review)
assert sched.has_current_head_approval(same_timestamp_review)
missing_review_time = make_pr(
reviews={
"nodes": [
Expand All @@ -331,14 +327,11 @@ def test_review_state_and_failed_checks():
]
}
)
assert not sched.has_current_head_approval(missing_review_time)
assert sched.stale_current_head_review_reason(missing_review_time) == (
"OpenCode review has no submission timestamp for the current head"
)
assert sched.has_current_head_approval(missing_review_time)
human_review_only = make_pr(
reviews={"nodes": [opencode_review("APPROVED", "head", login="human")]}
)
assert sched.stale_current_head_review_reason(human_review_only) is None
assert not sched.has_current_head_approval(human_review_only)
superseded = make_pr(
reviews={
"nodes": [
Expand Down Expand Up @@ -530,16 +523,16 @@ 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"
)
stale_auto = make_pr(
same_head_auto = make_pr(
autoMergeRequest={"enabledAt": "now"},
reviews={"nodes": [opencode_review("APPROVED", "head", submitted_at="2026-06-25T06:59:59Z")]},
)
disabled = []
monkeypatch.setattr(sched, "disable_auto_merge", lambda repo, pr, dry_run: disabled.append((repo, pr["number"], dry_run)))
stale_auto_decision = inspect(stale_auto)
assert stale_auto_decision.action == "disable_auto_merge"
assert "does not postdate the current head commit" in stale_auto_decision.reason
assert disabled == [("owner/repo", 1, True)]
same_head_auto_decision = inspect(same_head_auto)
assert same_head_auto_decision.action == "wait"
assert same_head_auto_decision.reason == "current head is approved; auto-merge already enabled"
assert disabled == []

stale_behind = make_pr(mergeStateStatus="BEHIND", reviews={"nodes": [opencode_review("APPROVED", "old")]})
dispatched = []
Expand Down