This is an automated email from the ASF dual-hosted git repository.

Yicong-Huang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git


The following commit(s) were added to refs/heads/main by this push:
     new 254faf8eac fix(ci): drop strict BEHIND filter in AutoQueue, iterate 
candidates (#4678)
254faf8eac is described below

commit 254faf8eac4199b305f4beaf0385944bb6703b05
Author: Yicong Huang <[email protected]>
AuthorDate: Sat May 2 03:05:21 2026 -0700

    fix(ci): drop strict BEHIND filter in AutoQueue, iterate candidates (#4678)
    
    ## What changes were proposed in this PR?
    
    In \`.github/workflows/auto-queue.yml\`, replace the \`mergeStateStatus
    === 'BEHIND'\` filter with an iteration over eligible auto-merge PRs
    (non-draft, non-conflicting). For each candidate, call \`updateBranch\`;
    on failure (already up-to-date / merge conflict / etc.), warn and try
    the next. Stop at the first PR that actually got updated.
    
    ## Any related issues, documentation, discussions?
    
    Follow-up to #4672. Observed in [run
    
25248773692](https://github.com/apache/texera/actions/runs/25248773692/job/74037400879):
    the workflow fired ~3s after a push to main, when GitHub had not yet
    recomputed \`mergeStateStatus\` for open PRs. PR #4652
    (\`mergeable=MERGEABLE\`, \`autoMergeRequest != null\`) was a real
    candidate but its \`mergeStateStatus\` was \`UNKNOWN\` at query time, so
    the strict \`=== 'BEHIND'\` filter dropped it and the run logged "No
    auto-merge PRs need updating".
    
    \`mergeStateStatus\` is documented as computed asynchronously, and
    there's no reliable bound on how long it stays \`UNKNOWN\` after a
    base-branch push. Letting \`updateBranch\` decide whether there's actual
    work to do is more robust than depending on a flaky pre-flight signal.
    
    ## How was this PR tested?
    
    Not yet — workflow runs only on push-to-main. Will observe behavior on
    the next merge.
    
    ## Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Opus 4.7 (Claude Code)
---
 .github/workflows/auto-queue.yml | 158 ++++++++++++++++++++++++++++++++-------
 1 file changed, 132 insertions(+), 26 deletions(-)

diff --git a/.github/workflows/auto-queue.yml b/.github/workflows/auto-queue.yml
index 74918cd2e3..6bdbde8cec 100644
--- a/.github/workflows/auto-queue.yml
+++ b/.github/workflows/auto-queue.yml
@@ -15,16 +15,36 @@
 # limitations under the License.
 
 # Temporary stand-in for GitHub Merge Queue.
-# After every push to main, picks the oldest auto-merge-enabled PR whose head
-# is behind main and merges main into it. If a PAT/App token with workflow
-# write is provided as AUTO_MERGE_TOKEN, the resulting push will retrigger the
-# PR's required checks and let auto-merge fire. With GITHUB_TOKEN only, the
-# branch is updated but downstream workflows on the PR are not retriggered.
-name: AutoQueue
+#
+# Triggers:
+#   * push to main: advance the queue right after a merge.
+#   * hourly cron: catch PRs that became BEHIND while no merge happened
+#     (e.g. a force-push to base, or a PR enabling auto-merge after the
+#     last main push).
+#   * workflow_dispatch: manual smoke test.
+#
+# Strategy: scan open PRs targeting main and pick the oldest eligible PR with
+# mergeStateStatus=BEHIND, then call updateBranch on it. A PR is eligible only
+# if it would actually merge once CI passes — auto-merge enabled, not a draft,
+# not conflicting, reviewDecision=APPROVED, and zero unresolved review threads.
+# This avoids burning CI on PRs blocked on review.
+#
+# mergeStateStatus is computed asynchronously and is UNKNOWN for a window
+# after a base-branch push. If at least one eligible PR is UNKNOWN, retry
+# with backoff up to ~2min to let it settle. If everything is settled and
+# nothing is BEHIND, exit without retrying — there's no work.
+#
+# Token: needs AUTO_MERGE_TOKEN with contents:write + pull_requests:write so
+# the resulting push retriggers required CI on the PR. Falls back to
+# GITHUB_TOKEN, in which case auto-merge will not actually fire (GITHUB_TOKEN
+# pushes don't trigger downstream workflows).
+name: Auto Queue
 
 on:
   push:
     branches: [main]
+  schedule:
+    - cron: '0 * * * *'
   workflow_dispatch:
 
 permissions:
@@ -45,6 +65,13 @@ jobs:
           script: |
             const { owner, repo } = context.repo;
 
+            const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
+            // 0, 10, 20, 30, 30, 30 = 120s total wall-clock budget across
+            // attempts. Short ramp catches the common case where
+            // mergeStateStatus settles within ~30s of a base-branch push;
+            // the tail keeps trying for the rare slow case.
+            const BACKOFFS_MS = [0, 10000, 20000, 30000, 30000, 30000];
+
             const query = `
               query($owner:String!, $name:String!) {
                 repository(owner:$owner, name:$name) {
@@ -60,33 +87,112 @@ jobs:
                       isDraft
                       mergeable
                       mergeStateStatus
+                      reviewDecision
                       autoMergeRequest { enabledAt }
+                      reviewThreads(first: 100) {
+                        nodes { isResolved }
+                      }
                     }
                   }
                 }
               }`;
 
-            const data = await github.graphql(query, { owner, name: repo });
-            const candidates = data.repository.pullRequests.nodes.filter(p =>
-              p.autoMergeRequest &&
-              !p.isDraft &&
-              p.mergeable !== 'CONFLICTING' &&
-              p.mergeStateStatus === 'BEHIND'
-            );
-
-            if (candidates.length === 0) {
-              core.info('No auto-merge PRs need updating.');
-              return;
+            function classify(p) {
+              if (!p.autoMergeRequest) return 'skip: auto-merge not enabled';
+              if (p.isDraft) return 'skip: draft';
+              if (p.mergeable === 'CONFLICTING') return 'skip: 
mergeable=CONFLICTING';
+              if (p.reviewDecision !== 'APPROVED') {
+                return `skip: reviewDecision=${p.reviewDecision || 'NONE'}`;
+              }
+              const threads = p.reviewThreads?.nodes ?? [];
+              const unresolved = threads.filter((t) => !t.isResolved).length;
+              if (unresolved > 0) {
+                return `skip: ${unresolved} unresolved review thread(s)`;
+              }
+              return `eligible: mergeable=${p.mergeable} 
state=${p.mergeStateStatus}`;
             }
 
-            const pr = candidates[0];
-            core.info(`Updating PR #${pr.number}: ${pr.title}`);
+            const start = Date.now();
+
+            for (let attempt = 0; attempt < BACKOFFS_MS.length; attempt++) {
+              if (BACKOFFS_MS[attempt] > 0) {
+                const elapsedS = Math.round((Date.now() - start) / 1000);
+                core.info(
+                  `Waiting ${BACKOFFS_MS[attempt] / 1000}s before attempt ` +
+                  `${attempt + 1}/${BACKOFFS_MS.length} (elapsed 
${elapsedS}s).`
+                );
+                await sleep(BACKOFFS_MS[attempt]);
+              }
+
+              core.startGroup(`Attempt ${attempt + 1}/${BACKOFFS_MS.length}`);
+              const data = await github.graphql(query, { owner, name: repo });
+              const all = data.repository.pullRequests.nodes;
+              core.info(`Scanned ${all.length} open PR(s) targeting main.`);
 
-            try {
-              await github.rest.pulls.updateBranch({
-                owner, repo, pull_number: pr.number,
-              });
-              core.info(`PR #${pr.number} update-branch dispatched.`);
-            } catch (e) {
-              core.setFailed(`updateBranch failed for #${pr.number}: 
${e.message}`);
+              const behind = [];
+              const unknown = [];
+              for (const p of all) {
+                const verdict = classify(p);
+                core.info(`  #${p.number} ${verdict} — ${p.title}`);
+                if (!verdict.startsWith('eligible')) continue;
+                if (p.mergeStateStatus === 'BEHIND') behind.push(p);
+                else if (p.mergeStateStatus === 'UNKNOWN') unknown.push(p);
+              }
+
+              core.info(
+                `Eligible: ${behind.length} BEHIND, ${unknown.length} UNKNOWN, 
` +
+                `rest already up-to-date or otherwise blocked.`
+              );
+
+              if (behind.length > 0) {
+                let updated = null;
+                for (const pr of behind) {
+                  core.info(`→ updateBranch #${pr.number}`);
+                  try {
+                    const res = await github.rest.pulls.updateBranch({
+                      owner, repo, pull_number: pr.number,
+                    });
+                    core.info(
+                      `✓ #${pr.number} updateBranch dispatched (HTTP 
${res.status}).`
+                    );
+                    updated = pr.number;
+                    break;
+                  } catch (e) {
+                    core.warning(
+                      `✗ #${pr.number} updateBranch failed ` +
+                      `(status ${e.status ?? '?'}): ${e.message}`
+                    );
+                  }
+                }
+                core.endGroup();
+                if (updated !== null) {
+                  core.info(`Done: #${updated} updated on attempt ${attempt + 
1}.`);
+                  return;
+                }
+                core.info(
+                  'All BEHIND PRs failed updateBranch this attempt; retrying 
after backoff.'
+                );
+                continue;
+              }
+
+              if (unknown.length > 0) {
+                core.info(
+                  `No BEHIND PRs yet; ${unknown.length} eligible PR(s) ` +
+                  'still UNKNOWN — retrying after backoff to let GitHub 
settle.'
+                );
+                core.endGroup();
+                continue;
+              }
+
+              core.info(
+                'No BEHIND or UNKNOWN eligible PRs — nothing to do this run.'
+              );
+              core.endGroup();
+              return;
             }
+
+            const totalS = Math.round((Date.now() - start) / 1000);
+            core.info(
+              `Exhausted ${BACKOFFS_MS.length} attempt(s) over ${totalS}s ` +
+              `without finding a BEHIND PR to update.`
+            );

Reply via email to