From 936a6accd7f26f386d6ed5767a5b717dcee96f26 Mon Sep 17 00:00:00 2001 From: Larry-Osakwe Date: Mon, 20 Jul 2026 10:32:38 -0700 Subject: [PATCH] ci: fast-forward main when the bump PR merge is refused The ruleset bypass covers ref pushes but the PR merge API does not exercise it. When the direct merge is refused, fast-forward main to the PR head via the refs API; GitHub marks the PR merged and the tag step proceeds at that SHA. Auto-merge stays armed and the recovery pre-check still backstops both. --- scripts/bump_package.py | 40 ++++++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/scripts/bump_package.py b/scripts/bump_package.py index 51edbd5..0254e9c 100644 --- a/scripts/bump_package.py +++ b/scripts/bump_package.py @@ -451,7 +451,7 @@ def checks_green(pr_data: dict) -> bool: return True -def wait_for_pr_merge(pr_number: int, timeout_seconds: int = 1800) -> str | None: +def wait_for_pr_merge(repo: str, pr_number: int, timeout_seconds: int = 1800) -> str | None: """Poll the PR until it merges. Returns the merge commit SHA on main. Fails if the PR is closed without merging or if the timeout elapses. @@ -470,7 +470,7 @@ def wait_for_pr_merge(pr_number: int, timeout_seconds: int = 1800) -> str | None "view", str(pr_number), "--json", - "state,mergeCommit,statusCheckRollup", + "state,mergeCommit,statusCheckRollup,headRefOid", ] ) if exit_code != 0: @@ -505,7 +505,10 @@ def wait_for_pr_merge(pr_number: int, timeout_seconds: int = 1800) -> str | None if state == "OPEN" and direct_merge_attempts < 3 and checks_green(data): # Auto-merge waits for requirements the app is entitled to bypass - # (required reviews); bypass only applies to an explicit merge. + # (required reviews), and the merge API does not exercise ruleset + # bypass either; ref updates do. Try the merge for the clean PR + # timeline, then fall back to fast-forwarding main to the PR head, + # which GitHub records as merging the PR. direct_merge_attempts += 1 exit_code, _, stderr = run_command( ["gh", "pr", "merge", str(pr_number), "--squash"] @@ -513,10 +516,31 @@ def wait_for_pr_merge(pr_number: int, timeout_seconds: int = 1800) -> str | None if exit_code == 0: print(f"Merged PR #{pr_number} directly as the bypass actor.") else: - print( - f"Direct merge attempt {direct_merge_attempts} refused; " - f"auto-merge stays armed: {stderr.strip()[:200]}" - ) + print(f"Direct merge refused: {stderr.strip()[:200]}") + head_sha = data.get("headRefOid") + if head_sha: + exit_code, _, stderr = run_command( + [ + "gh", + "api", + "-X", + "PATCH", + f"repos/{repo}/git/refs/heads/main", + "-f", + f"sha={head_sha}", + ] + ) + if exit_code == 0: + print( + f"Fast-forwarded main to {head_sha[:8]}; " + f"PR #{pr_number} will be marked merged." + ) + else: + print( + f"Fast-forward attempt {direct_merge_attempts} failed " + f"(main may have moved); auto-merge stays armed: " + f"{stderr.strip()[:200]}" + ) time.sleep(30) @@ -637,7 +661,7 @@ def bump_package(package_name: str, package_dir: str) -> bool: if pr_number is None: return False - merge_sha = wait_for_pr_merge(pr_number) + merge_sha = wait_for_pr_merge(repo, pr_number) if merge_sha is None: return False