From 0ce74123965fac51aa15ba0463b55923d7075d20 Mon Sep 17 00:00:00 2001 From: m3taversal Date: Thu, 16 Apr 2026 13:53:31 +0100 Subject: [PATCH] fix: check Forgejo close return value in 2 merge.py paths to prevent ghost PRs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both the "already merged" path and _handle_permanent_conflicts closed PRs on Forgejo without checking the return value. On API failure, the DB update would proceed anyway, creating ghost PRs (DB=closed/merged, Forgejo=open). Now both paths check for None return and skip DB updates on failure — same pattern as close_pr in pr_state.py. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/merge.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/lib/merge.py b/lib/merge.py index 9910fb8..f596446 100644 --- a/lib/merge.py +++ b/lib/merge.py @@ -746,12 +746,16 @@ async def _merge_domain_queue(conn, domain: str) -> tuple[int, int]: # The branch ref still points at old commits (not a descendant of main), # so pushing branch_sha:main would fail as non-fast-forward. if pick_msg in ("already merged (all commits empty)", "already up to date"): - mark_merged(conn, pr_num) - db.audit(conn, "merge", "merged", json.dumps({"pr": pr_num, "branch": branch, "note": "content already on main"})) leo_token = get_agent_token("leo") await forgejo_api("POST", repo_path(f"issues/{pr_num}/comments"), {"body": f"Content already on main — closing.\nBranch: `{branch}`"}) - await forgejo_api("PATCH", repo_path(f"pulls/{pr_num}"), {"state": "closed"}, token=leo_token) + result = await forgejo_api("PATCH", repo_path(f"pulls/{pr_num}"), {"state": "closed"}, token=leo_token) + if result is None: + logger.error("PR #%d: Forgejo close failed (already-merged path), skipping DB update", pr_num) + failed += 1 + continue + mark_merged(conn, pr_num) + db.audit(conn, "merge", "merged", json.dumps({"pr": pr_num, "branch": branch, "note": "content already on main"})) await _delete_remote_branch(branch) logger.info("PR #%d already merged (content on main), closed", pr_num) succeeded += 1 @@ -964,12 +968,15 @@ async def _handle_permanent_conflicts(conn) -> int: branch = row["branch"] domain = row["domain"] or "unknown" - # Close PR on Forgejo - await forgejo_api( + # Close PR on Forgejo — check return to prevent ghost PR + close_result = await forgejo_api( "PATCH", repo_path(f"pulls/{pr_number}"), body={"state": "closed"}, ) + if close_result is None: + logger.error("PR #%d: Forgejo close failed (permanent conflict), skipping", pr_number) + continue await forgejo_api( "POST", repo_path(f"issues/{pr_number}/comments"),