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
46 changes: 29 additions & 17 deletions scripts/ci/pr_review_merge_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -1028,6 +1028,19 @@ def decide(action: str, reason: str) -> Decision:
return decide("block", "current-head OpenCode review requested changes")

current_head_approved = has_current_head_approval(pr)
if merge_state == "BEHIND" and current_head_approved:
if not update_branches:
return decide("wait", "current-head OpenCode review approved; branch update disabled")
if not can_update_pr_head(repo, pr):
return decide("wait", non_mutable_head_reason(repo, pr))
update_branch(repo, pr, dry_run=dry_run)
suffix = "; existing auto-merge request remains queued" if pr.get("autoMergeRequest") else ""
return decide(
"update_branch",
"current-head OpenCode review approved; branch update requested with workflow GH_TOKEN "
f"(github-actions[bot] in GitHub Actions){suffix}",
)

if current_head_approved:
failed_checks = failed_status_checks(pr)
if failed_checks:
Expand Down Expand Up @@ -1056,21 +1069,6 @@ def decide(action: str, reason: str) -> Decision:
)
return decide("wait", reason)

if merge_state == "BEHIND" and current_head_approved:
if not update_branches:
return decide("wait", "current-head OpenCode review approved; branch update disabled")
if not can_update_pr_head(repo, pr):
return decide("wait", non_mutable_head_reason(repo, pr))
had_auto_merge = bool(pr.get("autoMergeRequest"))
if had_auto_merge:
disable_auto_merge(repo, pr, dry_run=dry_run)
update_branch(repo, pr, dry_run=dry_run)
prefix = "auto-merge disabled before branch update; " if had_auto_merge else ""
return decide(
"update_branch",
f"{prefix}current-head OpenCode review approved; branch update requested with workflow GH_TOKEN (github-actions[bot] in GitHub Actions)",
)

if current_head_approved:
if pr.get("autoMergeRequest"):
return decide("wait", auto_merge_wait_reason(merge_state))
Expand Down Expand Up @@ -1318,6 +1316,7 @@ def update_branch_summary(decisions: list[Decision]) -> list[str]:
"",
f"Requested `update-branch` for PR {pr_list} with the workflow `GITHUB_TOKEN`, guarded by the observed `expected_head_sha`.",
"This is intentionally done inside GitHub Actions, not from a maintainer's local `gh` credential, so the mechanical update is attributable to the automation actor.",
"Existing native auto-merge requests stay queued; branch freshness should not be repaired by disabling auto-merge first.",
"The scheduler refuses a non-dry-run `update-branch` outside GitHub Actions; dispatch the workflow instead of running the mutation locally.",
"This branch-update API path needs `pull-requests: write`; it does not require the scheduler job to widen repository `contents` to write.",
"When repository permissions allow the mutation, GitHub records the resulting branch update as `github-actions[bot]`.",
Expand Down Expand Up @@ -1719,8 +1718,6 @@ def self_test() -> None:
base_branch="main",
)
assert decision.action == "update_branch"
assert "auto-merge disabled before branch update" in decision.reason
sample["autoMergeRequest"] = None
sample["statusCheckRollup"]["contexts"]["nodes"] = [
{"__typename": "CheckRun", "name": "strix", "status": "COMPLETED", "conclusion": "FAILURE"}
]
Expand All @@ -1735,6 +1732,21 @@ def self_test() -> None:
security_workflow="Strix Security Scan",
base_branch="main",
)
assert decision.action == "update_branch"
assert "existing auto-merge request remains queued" in decision.reason
sample["autoMergeRequest"] = None
sample["mergeStateStatus"] = "CLEAN"
decision = inspect_pr(
"owner/repo",
sample,
dry_run=True,
trigger_reviews=True,
enable_auto_merge_flag=True,
update_branches=True,
workflow="OpenCode Review",
security_workflow="Strix Security Scan",
base_branch="main",
)
assert decision.action == "block"
assert decision.reason == "failed check(s): strix"
sample["statusCheckRollup"]["contexts"]["nodes"] = []
Expand Down
20 changes: 13 additions & 7 deletions tests/test_pr_review_merge_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -1034,9 +1034,10 @@ def test_inspect_pr_blocks_and_waits_for_policy_states(monkeypatch):
statusCheckRollup={"contexts": {"nodes": [{"__typename": "CheckRun", "name": "strix", "conclusion": "FAILURE"}]}},
)
failed_decision = inspect(behind_failed)
assert failed_decision.action == "block"
assert failed_decision.reason == "failed check(s): strix"
assert called == []
assert failed_decision.action == "update_branch"
assert "workflow GH_TOKEN" in failed_decision.reason
assert called == [("owner/repo", 1, True)]
called.clear()
mixed_failure_and_action_required = make_pr(
reviews={"nodes": [opencode_review("APPROVED", "head")]},
statusCheckRollup={
Expand All @@ -1062,16 +1063,21 @@ def test_inspect_pr_blocks_and_waits_for_policy_states(monkeypatch):
},
)
action_required_decision = inspect(behind_action_required)
assert action_required_decision.action == "wait"
assert "workflow action required: opencode-review" in action_required_decision.reason
assert called == []
assert action_required_decision.action == "update_branch"
assert "workflow GH_TOKEN" in action_required_decision.reason
assert called == [("owner/repo", 1, True)]
called.clear()
behind_auto_merge_enabled = make_pr(
mergeStateStatus="BEHIND",
reviews={"nodes": [opencode_review("APPROVED", "head")]},
autoMergeRequest={"enabledAt": "now"},
)
assert inspect(behind_auto_merge_enabled).action == "update_branch"
disabled.clear()
behind_auto_merge_decision = inspect(behind_auto_merge_enabled)
assert behind_auto_merge_decision.action == "update_branch"
assert "existing auto-merge request remains queued" in behind_auto_merge_decision.reason
assert called == [("owner/repo", 1, True)]
assert disabled == []
called.clear()
rest_behind = make_pr(
mergeStateStatus="CLEAN",
Expand Down
Loading