diff --git a/scripts/ci/pr_review_merge_scheduler.py b/scripts/ci/pr_review_merge_scheduler.py index 3789f2bb9..9d55a68d8 100644 --- a/scripts/ci/pr_review_merge_scheduler.py +++ b/scripts/ci/pr_review_merge_scheduler.py @@ -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: @@ -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)) @@ -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]`.", @@ -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"} ] @@ -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"] = [] diff --git a/tests/test_pr_review_merge_scheduler.py b/tests/test_pr_review_merge_scheduler.py index 4e6e9b444..80a97d380 100644 --- a/tests/test_pr_review_merge_scheduler.py +++ b/tests/test_pr_review_merge_scheduler.py @@ -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={ @@ -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",