You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@texera.apache.org by gi...@apache.org on 2026/07/28 07:23:08 UTC

(texera) branch gh-readonly-queue/main/pr-6970-5040cadf383049edf90df4195d35066379d06c4e created (now a376439d76)

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

github-merge-queue[bot] pushed a change to branch gh-readonly-queue/main/pr-6970-5040cadf383049edf90df4195d35066379d06c4e
in repository https://gitbox.apache.org/repos/asf/texera.git


      at a376439d76 fix(ci): fix backport auto-label and skip fix(ci) from backport (#6970)

This branch includes the following new commits:

     new a376439d76 fix(ci): fix backport auto-label and skip fix(ci) from backport (#6970)

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(texera) 01/01: fix(ci): fix backport auto-label and skip fix(ci) from backport (#6970)

Posted by gi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-6970-5040cadf383049edf90df4195d35066379d06c4e
in repository https://gitbox.apache.org/repos/asf/texera.git

commit a376439d763988e2bf236b460afa67c93fea6018
Author: Yicong Huang <17...@users.noreply.github.com>
AuthorDate: Tue Jul 28 00:22:46 2026 -0700

    fix(ci): fix backport auto-label and skip fix(ci) from backport (#6970)
    
    ### What changes were proposed in this PR?
    
    The Backport Auto Label workflow crashed on every PR into `main` with
    `Cannot find module '@actions/github'`: it built a second Octokit via
    `require("@actions/github")`, but github-script's `require` resolves
    from the checked-out workspace, which has no such package. Regressed in
    #6962.
    
    Instead of github-script's `__original_require__` escape hatch, this
    splits the single step into two, each with its own `github-token`, so no
    second Octokit is built in-script:
    
    - **Label** (PAT): all reads and label writes, so a `labeled` event
    still retriggers the backport pre-merge check.
    - **Report** (GITHUB_TOKEN): requests reviewers and upserts the report
    comment as `github-actions[bot]`.
    
    Also skips `fix(ci):` PRs — CI-only fixes are never backported. Behavior
    is otherwise unchanged.
    
    ### Any related issues, documentation, discussions?
    
    Closes #6969
    
    ### How was this PR tested?
    
    - `node --check` on both script bodies and a YAML parse of the workflow.
    - This PR's own Backport Auto Label check stays red until merge:
    `pull_request_target` runs the base-branch (still-broken) workflow.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Claude Opus 4.8)
    
    Co-authored-by: Claude Opus 4.8 (1M context) <no...@anthropic.com>
---
 .github/workflows/backport-auto-label.yml | 438 +++++++++++++++++-------------
 1 file changed, 251 insertions(+), 187 deletions(-)

diff --git a/.github/workflows/backport-auto-label.yml b/.github/workflows/backport-auto-label.yml
index 12be8403b9..e99a93abee 100644
--- a/.github/workflows/backport-auto-label.yml
+++ b/.github/workflows/backport-auto-label.yml
@@ -19,7 +19,8 @@
 # .github/release-branches.yml and request review from each branch's release
 # manager. The author (or a committer) removes a release/* label to decline the
 # backport for that branch — a removal is remembered (via the timeline), so a
-# later edit never silently re-adds a label the author took off.
+# later edit never silently re-adds a label the author took off. `fix(ci):` PRs
+# only touch CI and are never backported, so they are left alone.
 #
 # Every decision is also written back to the PR as a single, in-place-updated
 # report comment: one row per actively-supported release branch saying whether
@@ -27,6 +28,18 @@
 # per-branch existence check only compares exact file paths, so a moved/renamed
 # file reads as "absent" even when the fix applies — the skip rows tell the
 # author to double-check and add the label by hand when that happens.
+#
+# Two tokens are needed, so the work is split across two github-script steps
+# rather than building a second Octokit inside one script (github-script's
+# patched `require` cannot resolve @actions/github from the workspace):
+#   - "Label" runs under the fine-grained PAT (falling back to the default
+#     token). Only labels added by the PAT retrigger the backport pre-merge
+#     check — labels added by the default GITHUB_TOKEN do not trigger
+#     downstream workflows. This step does every read and every label write,
+#     then hands the review/report decisions to the next step.
+#   - "Report" runs under the default GITHUB_TOKEN so the review request and
+#     the report comment are attributed to github-actions[bot], not the PAT
+#     owner, and do not retrigger any comment-/review-driven workflow.
 
 name: Backport Auto Label
 
@@ -65,38 +78,240 @@ jobs:
           echo "entries=${entries}" >> "$GITHUB_OUTPUT"
           echo "Release branch targets: ${entries}"
 
-      - name: Label fix PRs and request release-manager review
+      # Label step: runs under the PAT so a resulting `labeled` event retriggers
+      # the backport pre-merge check. It does all reads and label writes, then
+      # emits the review/report decisions for the bot-authored step below.
+      - name: Label fix PRs
+        id: label
         uses: actions/github-script@v9
         env:
           ENTRIES: ${{ steps.targets.outputs.entries }}
-          BOT_TOKEN: ${{ secrets.GITHUB_TOKEN }}
         with:
-          # Label ops use the fine-grained PAT (falling back to the default
-          # token) so the resulting `labeled` event retriggers the backport
-          # pre-merge check — labels applied by the default GITHUB_TOKEN do not
-          # trigger downstream workflows. The review request and the report
-          # comment are sent by the bot (GITHUB_TOKEN, via BOT_TOKEN below) so
-          # they are attributed to github-actions[bot] rather than the PAT
-          # owner; neither needs to trigger a downstream workflow.
           github-token: ${{ secrets.AUTO_MERGE_TOKEN || secrets.GITHUB_TOKEN }}
           script: |
             const entries = JSON.parse(process.env.ENTRIES || "[]");
-            if (entries.length === 0) {
-              core.info("No release branches configured; nothing to label.");
+            const { owner, repo } = context.repo;
+            const pr = context.payload.pull_request;
+            const title = pr.title || "";
+
+            // Everything the report step needs, decided here and handed over as
+            // a single JSON output. mode ∈ none | no-backport | report.
+            //   rows:           one per actively-supported branch { branch, status, note }
+            //   reviewRequests: managers to ping for the labels we just added
+            async function decide() {
+              if (entries.length === 0) {
+                core.info("No release branches configured; nothing to label.");
+                return { mode: "none" };
+              }
+
+              // Conventional-commit `fix:` (optional scope, optional `!`). Other
+              // types (feat/chore/docs/...) are not auto-backported and, since
+              // they are never backport candidates, get no report comment.
+              const m = title.match(/^fix(?:\(([^)]*)\))?!?:/i);
+              if (!m) {
+                core.info(`PR #${pr.number} title is not a fix (\"${title}\"); skipping.`);
+                return { mode: "none" };
+              }
+
+              // `fix(ci):` touches CI/tooling only; it never lands on a release
+              // branch, so treat it like a non-fix and skip auto-labeling.
+              const scopes = (m[1] || "")
+                .split(",")
+                .map((s) => s.trim().toLowerCase())
+                .filter(Boolean);
+              if (scopes.includes("ci")) {
+                core.info(`PR #${pr.number} is fix(ci) — CI-only, never backported; skipping.`);
+                return { mode: "none" };
+              }
+
+              const author = pr.user?.login;
+              const currentLabels = new Set((pr.labels || []).map((l) => l.name));
+
+              // Hard manual override: a maintainer marks a PR that must never be
+              // backported (e.g. it fixes a feature that only exists on main).
+              if (currentLabels.has("no-backport-needed")) {
+                core.info(`PR #${pr.number} has no-backport-needed; skipping all backport labels.`);
+                return { mode: "no-backport" };
+              }
+
+              // A fix for a feature that does not exist on a release branch
+              // should not be backported there. The reliable, cheap signal: the
+              // files the PR *modifies* (not the ones it adds — a backportable
+              // fix may add files too) don't exist on that branch at all. If
+              // every modified file is absent, the feature isn't there, so skip
+              // the target. Any ambiguity (a modified file still exists, or the
+              // existence check errors) keeps the label and lets the apply-check
+              // decide — we would rather over-label than drop a real backport.
+              // Returns the list of modified paths it checked so the report can
+              // name them when the branch looks absent.
+              async function analyzeBranch(prNumber, branch) {
+                const files = await github.paginate(github.rest.pulls.listFiles, {
+                  owner, repo, pull_number: prNumber, per_page: 100,
+                });
+                const preexisting = files
+                  .filter((f) => f.status !== "added")
+                  .map((f) => (f.status === "renamed" ? f.previous_filename : f.filename));
+                if (preexisting.length === 0) {
+                  return { absent: false, checked: [] }; // pure-add fix applies cleanly
+                }
+                for (const path of preexisting) {
+                  try {
+                    await github.rest.repos.getContent({ owner, repo, path, ref: branch });
+                    return { absent: false, checked: preexisting }; // a modified file exists
+                  } catch (e) {
+                    if (e.status === 404) continue;
+                    core.warning(`getContent ${path}@${branch} failed (${e.status ?? "?"}); assuming present.`);
+                    return { absent: false, checked: preexisting };
+                  }
+                }
+                return { absent: true, checked: preexisting }; // every modified file absent
+              }
+
+              // Labels the author (or anyone) explicitly removed. Re-adding one
+              // would override a deliberate opt-out, so we skip those forever.
+              const timeline = await github.paginate(
+                github.rest.issues.listEventsForTimeline,
+                { owner, repo, issue_number: pr.number, per_page: 100 }
+              );
+              const removed = new Set(
+                timeline
+                  .filter((e) => e.event === "unlabeled" && e.label?.name)
+                  .map((e) => e.label.name)
+              );
+
+              async function ensureLabelExists(name) {
+                try {
+                  await github.rest.issues.getLabel({ owner, repo, name });
+                } catch (e) {
+                  if (e.status !== 404) throw e;
+                  // Match the colour of the existing release/* labels.
+                  await github.rest.issues.createLabel({
+                    owner,
+                    repo,
+                    name,
+                    color: "ccebc2",
+                    description: `back porting to ${name}`,
+                  });
+                  core.info(`Created missing label ${name}.`);
+                }
+              }
+
+              // One row per actively-supported branch: { branch, status, note }.
+              // status ∈ labeled | skipped | declined. reviewRequests holds the
+              // { manager, branch } pings the report step sends as the bot.
+              const rows = [];
+              const reviewRequests = [];
+
+              for (const entry of entries) {
+                const label = entry.branch;
+                const manager = entry.manager;
+
+                // Only actively-supporting branches are analyzed / auto-labeled.
+                // An inactive branch stays a valid manual target (label it by
+                // hand and the apply-check / post-merge backport still run) — it
+                // just isn't offered by default and is left out of the report.
+                if (!entry.active) {
+                  core.info(`${label} is not actively-supporting; not auto-labeling.`);
+                  continue;
+                }
+                if (currentLabels.has(label)) {
+                  core.info(`PR #${pr.number} already has ${label}.`);
+                  rows.push({
+                    branch: label,
+                    status: "labeled",
+                    note: "Already labeled — this fix is queued to backport here.",
+                  });
+                  continue;
+                }
+                if (removed.has(label)) {
+                  core.info(`${label} was removed on PR #${pr.number} (opt-out); not re-adding.`);
+                  rows.push({
+                    branch: label,
+                    status: "declined",
+                    note:
+                      "Label was removed earlier (opt-out); not re-added. Re-add " +
+                      "it by hand if this fix should be backported here after all.",
+                  });
+                  continue;
+                }
+
+                const analysis = await analyzeBranch(pr.number, label);
+                if (analysis.absent) {
+                  core.info(`PR #${pr.number}: all modified files absent on ${label}; feature not on that release — skipping.`);
+                  const fileList = analysis.checked
+                    .map((f) => `\`${f}\``)
+                    .join(", ");
+                  rows.push({
+                    branch: label,
+                    status: "skipped",
+                    note:
+                      "Not labeled automatically — none of the files this PR " +
+                      `modifies exist on this branch (${fileList}). The fix may ` +
+                      "target code that isn't on this release, or the files were " +
+                      "moved/renamed after the branch was cut. **Please check and " +
+                      `add \`${label}\` by hand if this fix should be backported here.**`,
+                  });
+                  continue;
+                }
+
+                await ensureLabelExists(label);
+                await github.rest.issues.addLabels({
+                  owner,
+                  repo,
+                  issue_number: pr.number,
+                  labels: [label],
+                });
+                core.info(`Added ${label} to PR #${pr.number}.`);
+
+                const note = analysis.checked.length > 0
+                  ? "Change detected on this branch — label added; this fix is queued to backport here."
+                  : "Label added by default (this fix only adds files); the pre-merge backport check confirms it applies.";
+                rows.push({ branch: label, status: "labeled", note });
+
+                if (manager && manager !== author) {
+                  reviewRequests.push({ manager, branch: label });
+                } else if (manager === author) {
+                  core.info(`Release manager ${manager} is the author; skipping review request.`);
+                }
+              }
+
+              if (rows.length === 0) {
+                core.info("No actively-supporting release branches to report.");
+                return { mode: "none" };
+              }
+              return { mode: "report", rows, reviewRequests };
+            }
+
+            // Only hand work to the report step when there is something to say;
+            // a "none" outcome leaves the output unset so that step is skipped.
+            const result = await decide();
+            if (result.mode !== "none") {
+              core.setOutput("result", JSON.stringify(result));
+            }
+
+      # Report step: runs under the default GITHUB_TOKEN so the review request
+      # and the report comment are authored by github-actions[bot] and do not
+      # retrigger comment-/review-driven workflows. It only touches the PR when
+      # the label step handed it something to say.
+      - name: Report backport decisions
+        if: ${{ steps.label.outputs.result }}
+        uses: actions/github-script@v9
+        env:
+          RESULT: ${{ steps.label.outputs.result }}
+        with:
+          github-token: ${{ secrets.GITHUB_TOKEN }}
+          script: |
+            const result = JSON.parse(process.env.RESULT || '{"mode":"none"}');
+            if (result.mode === "none") {
+              core.info("Nothing to report.");
               return;
             }
 
             const { owner, repo } = context.repo;
             const pr = context.payload.pull_request;
-            const title = pr.title || "";
             const runUrl =
               `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`;
 
-            // The bot octokit (GITHUB_TOKEN) authors the review request and the
-            // report comment as github-actions[bot]; neither needs to retrigger
-            // a workflow, so they do not use the label PAT.
-            const bot = require("@actions/github").getOctokit(process.env.BOT_TOKEN);
-
             // A single hidden marker lets us update one report comment in place
             // rather than stack a fresh comment on every `edited` event.
             // Editing a comment sends no new notification, so re-runs are quiet.
@@ -112,12 +327,12 @@ jobs:
               );
               try {
                 if (existing) {
-                  await bot.rest.issues.updateComment({
+                  await github.rest.issues.updateComment({
                     owner, repo, comment_id: existing.id, body,
                   });
                   core.info(`Updated backport report comment ${existing.id}.`);
                 } else {
-                  await bot.rest.issues.createComment({
+                  await github.rest.issues.createComment({
                     owner, repo, issue_number: pr.number, body,
                   });
                   core.info("Created backport report comment.");
@@ -127,21 +342,7 @@ jobs:
               }
             }
 
-            // Conventional-commit `fix:` (optional scope, optional `!`). Other
-            // types (feat/chore/docs/...) are not auto-backported and, since
-            // they are never backport candidates, get no report comment.
-            if (!/^fix(\([^)]*\))?!?:/i.test(title)) {
-              core.info(`PR #${pr.number} title is not a fix (\"${title}\"); skipping.`);
-              return;
-            }
-
-            const author = pr.user?.login;
-            const currentLabels = new Set((pr.labels || []).map((l) => l.name));
-
-            // Hard manual override: a maintainer marks a PR that must never be
-            // backported (e.g. it fixes a feature that only exists on main).
-            if (currentLabels.has("no-backport-needed")) {
-              core.info(`PR #${pr.number} has no-backport-needed; skipping all backport labels.`);
+            if (result.mode === "no-backport") {
               await upsertReport(
                 "### Backport auto-label report\n\n" +
                 "This `fix:` PR carries **`no-backport-needed`**, so no release " +
@@ -152,167 +353,30 @@ jobs:
               return;
             }
 
-            // A fix for a feature that does not exist on a release branch
-            // should not be backported there. The reliable, cheap signal: the
-            // files the PR *modifies* (not the ones it adds — a backportable
-            // fix may add files too) don't exist on that branch at all. If
-            // every modified file is absent, the feature isn't there, so skip
-            // the target. Any ambiguity (a modified file still exists, or the
-            // existence check errors) keeps the label and lets the apply-check
-            // decide — we would rather over-label than drop a real backport.
-            // Returns the list of modified paths it checked so the report can
-            // name them when the branch looks absent.
-            async function analyzeBranch(prNumber, branch) {
-              const files = await github.paginate(github.rest.pulls.listFiles, {
-                owner, repo, pull_number: prNumber, per_page: 100,
-              });
-              const preexisting = files
-                .filter((f) => f.status !== "added")
-                .map((f) => (f.status === "renamed" ? f.previous_filename : f.filename));
-              if (preexisting.length === 0) {
-                return { absent: false, checked: [] }; // pure-add fix applies cleanly
-              }
-              for (const path of preexisting) {
-                try {
-                  await github.rest.repos.getContent({ owner, repo, path, ref: branch });
-                  return { absent: false, checked: preexisting }; // a modified file exists
-                } catch (e) {
-                  if (e.status === 404) continue;
-                  core.warning(`getContent ${path}@${branch} failed (${e.status ?? "?"}); assuming present.`);
-                  return { absent: false, checked: preexisting };
-                }
-              }
-              return { absent: true, checked: preexisting }; // every modified file absent
-            }
-
-            // Labels the author (or anyone) explicitly removed. Re-adding one
-            // would override a deliberate opt-out, so we skip those forever.
-            const timeline = await github.paginate(
-              github.rest.issues.listEventsForTimeline,
-              { owner, repo, issue_number: pr.number, per_page: 100 }
-            );
-            const removed = new Set(
-              timeline
-                .filter((e) => e.event === "unlabeled" && e.label?.name)
-                .map((e) => e.label.name)
-            );
-
-            async function ensureLabelExists(name) {
+            // Request review from each labeled branch's release manager as the
+            // bot, then note the successful pings on the matching report row.
+            const rowByBranch = new Map(result.rows.map((r) => [r.branch, r]));
+            for (const req of result.reviewRequests || []) {
               try {
-                await github.rest.issues.getLabel({ owner, repo, name });
-              } catch (e) {
-                if (e.status !== 404) throw e;
-                // Match the colour of the existing release/* labels.
-                await github.rest.issues.createLabel({
+                await github.rest.pulls.requestReviewers({
                   owner,
                   repo,
-                  name,
-                  color: "ccebc2",
-                  description: `back porting to ${name}`,
+                  pull_number: pr.number,
+                  reviewers: [req.manager],
                 });
-                core.info(`Created missing label ${name}.`);
-              }
-            }
-
-            // One row per actively-supported branch: { branch, status, note }.
-            // status ∈ labeled | skipped | declined.
-            const reports = [];
-
-            for (const entry of entries) {
-              const label = entry.branch;
-              const manager = entry.manager;
-
-              // Only actively-supporting branches are analyzed / auto-labeled.
-              // An inactive branch stays a valid manual target (label it by
-              // hand and the apply-check / post-merge backport still run) — it
-              // just isn't offered by default and is left out of the report.
-              if (!entry.active) {
-                core.info(`${label} is not actively-supporting; not auto-labeling.`);
-                continue;
-              }
-              if (currentLabels.has(label)) {
-                core.info(`PR #${pr.number} already has ${label}.`);
-                reports.push({
-                  branch: label,
-                  status: "labeled",
-                  note: "Already labeled — this fix is queued to backport here.",
-                });
-                continue;
-              }
-              if (removed.has(label)) {
-                core.info(`${label} was removed on PR #${pr.number} (opt-out); not re-adding.`);
-                reports.push({
-                  branch: label,
-                  status: "declined",
-                  note:
-                    "Label was removed earlier (opt-out); not re-added. Re-add " +
-                    "it by hand if this fix should be backported here after all.",
-                });
-                continue;
-              }
-
-              const analysis = await analyzeBranch(pr.number, label);
-              if (analysis.absent) {
-                core.info(`PR #${pr.number}: all modified files absent on ${label}; feature not on that release — skipping.`);
-                const fileList = analysis.checked
-                  .map((f) => `\`${f}\``)
-                  .join(", ");
-                reports.push({
-                  branch: label,
-                  status: "skipped",
-                  note:
-                    "Not labeled automatically — none of the files this PR " +
-                    `modifies exist on this branch (${fileList}). The fix may ` +
-                    "target code that isn't on this release, or the files were " +
-                    "moved/renamed after the branch was cut. **Please check and " +
-                    `add \`${label}\` by hand if this fix should be backported here.**`,
-                });
-                continue;
-              }
-
-              await ensureLabelExists(label);
-              await github.rest.issues.addLabels({
-                owner,
-                repo,
-                issue_number: pr.number,
-                labels: [label],
-              });
-              core.info(`Added ${label} to PR #${pr.number}.`);
-
-              let note = analysis.checked.length > 0
-                ? "Change detected on this branch — label added; this fix is queued to backport here."
-                : "Label added by default (this fix only adds files); the pre-merge backport check confirms it applies.";
-
-              if (manager && manager !== author) {
-                try {
-                  await bot.rest.pulls.requestReviewers({
-                    owner,
-                    repo,
-                    pull_number: pr.number,
-                    reviewers: [manager],
-                  });
-                  core.info(`Requested review from ${manager} for ${label}.`);
-                  note += ` Requested review from @${manager}.`;
-                } catch (e) {
-                  core.warning(
-                    `Could not request review from ${manager} (status ${e.status ?? "?"}): ${e.message}`
-                  );
-                }
-              } else if (manager === author) {
-                core.info(`Release manager ${manager} is the author; skipping review request.`);
+                core.info(`Requested review from ${req.manager} for ${req.branch}.`);
+                const row = rowByBranch.get(req.branch);
+                if (row) row.note += ` Requested review from @${req.manager}.`;
+              } catch (e) {
+                core.warning(
+                  `Could not request review from ${req.manager} (status ${e.status ?? "?"}): ${e.message}`
+                );
               }
-
-              reports.push({ branch: label, status: "labeled", note });
-            }
-
-            if (reports.length === 0) {
-              core.info("No actively-supporting release branches to report.");
-              return;
             }
 
             // Render the per-branch decisions as a single report comment.
             const emoji = { labeled: "✅", skipped: "⚠️", declined: "🚫" };
-            const rows = reports
+            const rows = result.rows
               .map((r) => `| ${emoji[r.status] || ""} \`${r.branch}\` | ${r.note} |`)
               .join("\n");
             await upsertReport(