diff --git a/.gitea/scripts/gitea-merge-queue.py b/.gitea/scripts/gitea-merge-queue.py index 46b0482ad..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: @@ -407,7 +430,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..b94d00f68 100644 --- a/.gitea/workflows/gitea-merge-queue.yml +++ b/.gitea/workflows/gitea-merge-queue.yml @@ -48,6 +48,12 @@ jobs: REQUIRED_CONTEXTS: >- CI / all-required (pull_request), 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