From 27b6df119c6cd3b78d4cf46473df52f49561a8b9 Mon Sep 17 00:00:00 2001 From: Molecule AI Infra-SRE Date: Fri, 15 May 2026 06:24:15 +0000 Subject: [PATCH 1/3] fix(merge-queue): add review gates and handle merge failures gracefully MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two fixes to the serialized Gitea merge queue: 1. REQUIRED_CONTEXTS now includes qa-review and security-review gates. Previously only CI/all-required and sop-checklist were checked, so PRs with failed reviews were merged (blocked by pre-receive hook) and retried forever — each tick re-attempting the same blocked PR. Adding the explicit review contexts causes the queue to WAIT instead of attempting merge, unblocking the next queued PR. 2. process_once() now catches ApiError on merge attempt and removes the merge-queue label rather than returning 0 and retrying the same PR on every subsequent tick. The comment on the PR informs the author what blocked the merge and tells them to re-add the label once resolved. Fixes: mc# queue infinite retry on review-blocked PRs Co-Authored-By: Claude Opus 4.7 --- .gitea/scripts/gitea-merge-queue.py | 25 ++++++++++++++++++++++++- .gitea/workflows/gitea-merge-queue.yml | 8 +++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/.gitea/scripts/gitea-merge-queue.py b/.gitea/scripts/gitea-merge-queue.py index 46b0482ad..63974350b 100644 --- a/.gitea/scripts/gitea-merge-queue.py +++ b/.gitea/scripts/gitea-merge-queue.py @@ -407,7 +407,30 @@ def process_once(*, dry_run: bool = False) -> int: "deferring to next tick" ) return 0 - merge_pull(pr_number, dry_run=dry_run) + try: + merge_pull(pr_number, dry_run=dry_run) + except ApiError as exc: + # Merge failed (pre-receive hook, branch protection, etc.). + # Remove queue label so next tick picks the next PR. + msg = str(exc) + if "405" in msg or "not allowed to merge" in msg.lower(): + hint = "pre-receive hook or branch protection blocked the merge" + elif "422" in msg or "Unprocessable" in msg: + hint = "branch protection required-status check failed" + elif "409" in msg or "conflict" in msg.lower(): + hint = "merge conflict" + else: + hint = msg[:200] + remove_label(pr_number, QUEUE_LABEL, dry_run=dry_run) + post_comment( + pr_number, + ( + f"merge-queue: merge blocked ({hint}). " + f"Label removed — re-add once the block is resolved." + ), + dry_run=dry_run, + ) + return 0 return 0 return 0 diff --git a/.gitea/workflows/gitea-merge-queue.yml b/.gitea/workflows/gitea-merge-queue.yml index 2ad090171..f87f7e4b2 100644 --- a/.gitea/workflows/gitea-merge-queue.yml +++ b/.gitea/workflows/gitea-merge-queue.yml @@ -47,7 +47,13 @@ jobs: UPDATE_STYLE: merge REQUIRED_CONTEXTS: >- CI / all-required (pull_request), - sop-checklist / all-items-acked (pull_request) + sop-checklist / all-items-acked (pull_request), + qa-review / approved (pull_request), + security-review / approved (pull_request) + # qa-review and security-review gates are included so PRs that fail + # review checks are dequeued automatically (label removed) rather than + # causing the queue to retry the same blocked PR on every tick. + # # Push-side required contexts. Checking CI / all-required (push) # explicitly instead of the combined state avoids false-pause when # non-blocking jobs (continue-on-error: true) have failed — those -- 2.52.0 From a1146d2f5f9d644a2a1106119cf738a3319a8cfd Mon Sep 17 00:00:00 2001 From: Molecule AI Infra-SRE Date: Fri, 15 May 2026 07:52:11 +0000 Subject: [PATCH 2/3] fix(merge-queue): remove broken qa/sec gates from REQUIRED_CONTEXTS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit qa-review and security-review gates permanently fail (mc#1111: SOP_TIER_CHECK_TOKEN missing PAT — token owner not in qa/security teams, HTTP 403 on team membership probe). Adding them to REQUIRED_CONTEXTS would cause the queue to strip the merge-queue label from every PR in the queue, breaking the queue for all contributors. Keep the ApiError error-handling from the previous commit (catches 405/422/409 from merge_pull and removes the label + posts a comment). That logic prevents infinite retries on blocked PRs even without qa/sec gates. Re-add qa-review and security-review to REQUIRED_CONTEXTS once mc#1111 is resolved. Co-Authored-By: Claude Opus 4.7 --- .gitea/workflows/gitea-merge-queue.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.gitea/workflows/gitea-merge-queue.yml b/.gitea/workflows/gitea-merge-queue.yml index f87f7e4b2..b94d00f68 100644 --- a/.gitea/workflows/gitea-merge-queue.yml +++ b/.gitea/workflows/gitea-merge-queue.yml @@ -47,13 +47,13 @@ jobs: UPDATE_STYLE: merge REQUIRED_CONTEXTS: >- CI / all-required (pull_request), - sop-checklist / all-items-acked (pull_request), - qa-review / approved (pull_request), - security-review / approved (pull_request) - # qa-review and security-review gates are included so PRs that fail - # review checks are dequeued automatically (label removed) rather than - # causing the queue to retry the same blocked PR on every tick. - # + sop-checklist / all-items-acked (pull_request) + # NOTE: qa-review / security-review gates intentionally omitted. + # These gates permanently fail (mc#1111: SOP_TIER_CHECK_TOKEN missing + # PAT — token owner not in qa/security teams). Adding them to + # REQUIRED_CONTEXTS would strip the merge-queue label from every PR + # in the queue, breaking the queue for all contributors. + # Re-add these gates once mc#1111 is resolved. # Push-side required contexts. Checking CI / all-required (push) # explicitly instead of the combined state avoids false-pause when # non-blocking jobs (continue-on-error: true) have failed — those -- 2.52.0 From e860114ef194efbadba2212aab0786fde73511dc Mon Sep 17 00:00:00 2001 From: Molecule AI Infra-SRE Date: Fri, 15 May 2026 09:15:08 +0000 Subject: [PATCH 3/3] fix(merge-queue): add remove_label function needed by ApiError handler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ApiError handler (added in 7c08352d) calls remove_label() to strip the queue label from PRs blocked by pre-receive hooks, but the function was never defined — causing NameError on the first merge failure and crashing the workflow tick. Fixes: mc#1144 (queue stalls after pre-receive hook 405) Co-Authored-By: Claude Opus 4.7 --- .gitea/scripts/gitea-merge-queue.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.gitea/scripts/gitea-merge-queue.py b/.gitea/scripts/gitea-merge-queue.py index 63974350b..91c86c975 100644 --- a/.gitea/scripts/gitea-merge-queue.py +++ b/.gitea/scripts/gitea-merge-queue.py @@ -314,6 +314,29 @@ def post_comment(pr_number: int, body: str, *, dry_run: bool) -> None: api("POST", f"/repos/{OWNER}/{NAME}/issues/{pr_number}/comments", body={"body": body}) +def remove_label(pr_number: int, label: str, *, dry_run: bool) -> None: + print(f"::notice::removing label '{label}' from PR #{pr_number}") + if dry_run: + return + # Gitea requires label ID, not name, for deletion. + # Multiple labels can share the same name with different IDs — remove all. + _, body = api("GET", f"/repos/{OWNER}/{NAME}/pulls/{pr_number}") + pr_labels = body.get("labels", []) if isinstance(body, dict) else [] + removed = False + for lbl in pr_labels: + if lbl.get("name") == label: + label_id = lbl.get("id") + if label_id: + api( + "DELETE", + f"/repos/{OWNER}/{NAME}/issues/{pr_number}/labels/{label_id}", + expect_json=False, + ) + removed = True + if not removed: + print(f"::notice::label '{label}' not found on PR #{pr_number}") + + def update_pull(pr_number: int, *, dry_run: bool) -> None: print(f"::notice::updating PR #{pr_number} with base branch via style={UPDATE_STYLE}") if dry_run: -- 2.52.0