From e2b35f43f5b48bfaaee033d92bbdd0458aca22ac Mon Sep 17 00:00:00 2001 From: Shrinath Rao Date: Wed, 19 Aug 2026 12:29:46 +0000 Subject: [PATCH] =?UTF-8?q?ci:=20Auto=20Merge=20Queuer=20=E2=80=94=20re-ad?= =?UTF-8?q?d=20PRs=20dequeued=20by=20failed=20checks=20(up=20to=202x)=20(#?= =?UTF-8?q?13891)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Adds `.github/workflows/merge-queue-auto-requeue.yml` — **Auto Merge Queuer**. When a PR is kicked **out of the merge queue because its required checks failed** (`reason == CI_FAILURE`) or **timed out** (`reason == CI_TIMEOUT`), the workflow adds it straight back to the queue — **up to twice**. ## Behavior - **Trigger:** `pull_request: [dequeued]`, gated at the job `if` to `CI_FAILURE` / `CI_TIMEOUT` only. - **Manual removals** (`reason == MANUAL`) and every other reason (`MERGE_CONFLICT`, `QUEUE_CLEARED`, `BRANCH_PROTECTIONS`, …) are **ignored** — the job never runs for them. - **Limit:** attempt count tracked via labels `auto-requeue-1` / `auto-requeue-2` (max 2 re-adds). - **Re-queue:** GraphQL `enqueuePullRequest` mutation, authenticated with `secrets.ORG_ADMIN_TOKEN` (a `GITHUB_TOKEN` enqueue would not re-trigger the `merge_group` check workflows). - **One sticky comment, edited in place** (found by a hidden marker): - 1st kick → "added back (attempt 1 of 2)" - 2nd kick → same comment edited → "re-added a second time (2 of 2) — last automatic retry" - 3rd kick → same comment edited → "🛑 no-go for auto re-add" and it does **not** re-queue. All `gh` calls run under the PAT, so the comment, labels and enqueue are authored by the token owner (not `github-actions[bot]`). ## Notes for reviewers - Requires repo secret `ORG_ADMIN_TOKEN` with merge-queue write (already used by 26 workflows in this repo). - `github.event.reason` on the `dequeued` payload drives the gate; worth confirming on the first real CI-caused dequeue. Safe failure mode if absent: the job no-ops. --- .../workflows/merge-queue-auto-requeue.yml | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 .github/workflows/merge-queue-auto-requeue.yml diff --git a/.github/workflows/merge-queue-auto-requeue.yml b/.github/workflows/merge-queue-auto-requeue.yml new file mode 100644 index 0000000000..4c84840741 --- /dev/null +++ b/.github/workflows/merge-queue-auto-requeue.yml @@ -0,0 +1,129 @@ +name: Auto Merge Queuer + +# When a PR is kicked OUT of the merge queue because its required checks failed +# (reason CI_FAILURE) or timed out (reason CI_TIMEOUT), add it straight back to +# the queue — up to twice. Manual removals (reason MANUAL) and every other reason +# (MERGE_CONFLICT, QUEUE_CLEARED, BRANCH_PROTECTIONS, …) are ignored: the job `if` +# below never runs for them. +# +# Attempt count lives in the labels auto-requeue-1 / auto-requeue-2. A single +# sticky comment (marked with the hidden MARKER) is EDITED in place on each event: +# attempt 1 -> "re-added (1 of 2)" +# attempt 2 -> same comment edited to "re-added a second time (2 of 2)" +# 3rd kick -> same comment edited to "no-go for auto re-add" (no re-queue) +# +# Re-queueing uses the PAT (secrets.ORG_ADMIN_TOKEN) on purpose: an enqueue made +# with the default GITHUB_TOKEN would not re-trigger the merge_group check +# workflows. This is the repo's standard human-PAT secret, so the sticky comment, +# labels and enqueue are all authored as the human (not github-actions[bot]). +# +# REQUIRED — secrets.ORG_ADMIN_TOKEN (fine-grained PAT) permissions: +# openobserve: Pull requests: R/W (enqueuePullRequest + sticky comment) +# Issues: R/W (label create + add) +# NOTE: the `permissions:` block below constrains ONLY the run's GITHUB_TOKEN — it +# does NOT scope the PAT. Prefer a fine-grained PAT limited to this repo so its +# reach matches the two write scopes above rather than full org-admin. + +on: + pull_request: + types: [dequeued] + +# Applies to the run's GITHUB_TOKEN only (unused for API calls here — those use the +# PAT); kept minimal so nothing this job doesn't need is granted to GITHUB_TOKEN. +permissions: + contents: read + +concurrency: + group: auto-requeue-${{ github.event.pull_request.number }} + cancel-in-progress: false + +jobs: + requeue: + # Only dequeues caused by failed/timed-out required checks. + if: github.event.reason == 'CI_FAILURE' || github.event.reason == 'CI_TIMEOUT' + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ secrets.ORG_ADMIN_TOKEN }} + REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_NODE_ID: ${{ github.event.pull_request.node_id }} + REASON: ${{ github.event.reason }} + MAX_ATTEMPTS: "2" + MARKER: "" + steps: + - name: Re-add to merge queue (up to twice) or mark no-go + run: | + set -euo pipefail + + echo "PR #${PR_NUMBER} dequeued with reason: ${REASON}" + + # --- current attempt count from labels ----------------------------- + labels="$(gh pr view "$PR_NUMBER" --repo "$REPO" --json labels --jq '.labels[].name')" + n=0 + for i in $(seq 1 "$MAX_ATTEMPTS"); do + if grep -qx "auto-requeue-$i" <<<"$labels"; then n=$i; fi + done + echo "Prior auto-requeue attempts: $n" + + # --- locate the sticky comment (if any) ---------------------------- + comment_id="$(gh api "repos/$REPO/issues/$PR_NUMBER/comments" --paginate \ + --jq "map(select(.body // \"\" | contains(\"$MARKER\"))) | last | .id // empty")" + + upsert_comment() { + # $1 = full comment body + local body="$1" + if [ -n "$comment_id" ]; then + gh api -X PATCH "repos/$REPO/issues/comments/$comment_id" -f body="$body" >/dev/null + echo "Edited sticky comment $comment_id" + else + comment_id="$(gh api "repos/$REPO/issues/$PR_NUMBER/comments" -f body="$body" --jq '.id')" + echo "Created sticky comment $comment_id" + fi + } + + # --- already used both retries -> give up -------------------------- + if [ "$n" -ge "$MAX_ATTEMPTS" ]; then + echo "Attempts exhausted ($n/$MAX_ATTEMPTS); not re-queueing." + upsert_comment "$MARKER + 🛑 **Auto-requeue exhausted** — this PR was dequeued again (reason \`${REASON}\`) after **${MAX_ATTEMPTS} automatic re-adds**. + + This is a **no-go for auto re-add**. Please investigate the failing required checks and re-queue manually once they're green." + exit 0 + fi + + # --- attempt n+1: enqueue via PAT ---------------------------------- + attempt=$((n + 1)) + echo "Re-adding to merge queue (attempt ${attempt}/${MAX_ATTEMPTS})" + + if ! gh api graphql -f query=' + mutation($id: ID!) { + enqueuePullRequest(input: { pullRequestId: $id }) { + mergeQueueEntry { state position } + } + }' -F id="$PR_NODE_ID" > enqueue.json; then + echo "::error::enqueuePullRequest failed" + cat enqueue.json || true + upsert_comment "$MARKER + ⚠️ **Auto-requeue failed** — tried to re-add this PR to the merge queue after a \`${REASON}\` dequeue but the enqueue API call failed. It has **not** been re-queued; please re-queue manually." + exit 1 + fi + cat enqueue.json + + # record the attempt so the next dequeue counts correctly + gh label create "auto-requeue-${attempt}" --repo "$REPO" \ + --color BFD4F2 --description "Auto re-added to the merge queue (attempt ${attempt})" \ + --force >/dev/null 2>&1 || true + gh pr edit "$PR_NUMBER" --repo "$REPO" --add-label "auto-requeue-${attempt}" + + # --- sticky comment for this attempt ------------------------------- + if [ "$attempt" -eq 1 ]; then + upsert_comment "$MARKER + 🔁 **Auto-requeue** — this PR was dequeued from the merge queue (reason \`${REASON}\`) and has been **added back (attempt 1 of ${MAX_ATTEMPTS})**. + + If it gets kicked out again it'll be re-added one more time, then left alone." + else + upsert_comment "$MARKER + 🔁 **Auto-requeue** — dequeued again (reason \`${REASON}\`) and **re-added a second time (attempt ${attempt} of ${MAX_ATTEMPTS})**. + + This is the **last** automatic retry. If it's kicked out once more, it won't be re-added automatically." + fi