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-6962-ee41d9a2a6d977ddc10b8cee0ef1f04da7b3ef31
in repository https://gitbox.apache.org/repos/asf/texera.git

commit 5040cadf383049edf90df4195d35066379d06c4e
Author: Yicong Huang <[email protected]>
AuthorDate: Mon Jul 27 22:31:14 2026 -0700

    ci: report backport auto-label decisions on the PR (#6962)
    
    ### What changes were proposed in this PR?
    
    The backport auto-label workflow
    (`.github/workflows/backport-auto-label.yml`) decides, per
    actively-supported release branch, whether to add a `release/*` label to
    a `fix:` PR. Until now every skip was silent — visible only in the
    Actions log. Its cheap "is this feature on the branch?" check compares
    **exact file paths**, so a file moved or renamed after the branch was
    cut reads as *absent* and the label is dropped with no signal to the
    author. #6960 hit exactly this: its files live under `coordinator/` on
    `main` but `controller/` on `release/v1.2`, so a fix that genuinely
    applied to v1.2 was never labeled.
    
    This PR makes the workflow write its reasoning back to the PR as a
    single report comment:
    
    - **One row per actively-supported release branch**, with the decision
    and why:
    - ✅ **labeled** — change detected on the branch; label added (and who
    was requested for review);
    - ⚠️ **skipped** — none of the modified files exist on the branch (the
    files are listed), with a prompt to check and add the label by hand if
    the fix should be backported;
      - 🚫 **declined** — a previously removed label (opt-out), not re-added.
    - The comment is **upserted in place** via a hidden marker, so `edited`
    re-runs update the same comment instead of stacking new ones (editing a
    comment sends no notification, so re-runs stay quiet).
    - It links the auto-label run, and is authored by `github-actions[bot]`
    like the existing review request (no label PAT needed).
    - Inactive branches and non-`fix:` PRs are unchanged (no comment); a
    `no-backport-needed` PR gets a one-line note instead of the table.
    
    Labeling behavior itself is unchanged — this only surfaces the decisions
    that were already being made.
    
    ### Any related issues, documentation, discussions?
    
    Resolves #6961. Follow-up to the backport lifecycle automation (#6941,
    #6959). Motivated by #6960, which was silently not labeled for
    `release/v1.2`.
    
    ### How was this PR tested?
    
    - YAML parses and the embedded github-script passes `node --check`
    (async-wrapped).
    - Walked the branch/label/timeline/file-status cases against #6960's
    real data to confirm the rows and skip reasons render as intended.
    - Full end-to-end exercise needs a live `pull_request_target` event with
    the org PAT/secrets, which only runs once merged.
    
    ### 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) <[email protected]>
---
 .github/workflows/backport-auto-label.yml | 148 ++++++++++++++++++++++++++----
 1 file changed, 130 insertions(+), 18 deletions(-)

diff --git a/.github/workflows/backport-auto-label.yml 
b/.github/workflows/backport-auto-label.yml
index 2e4c22d077..12be8403b9 100644
--- a/.github/workflows/backport-auto-label.yml
+++ b/.github/workflows/backport-auto-label.yml
@@ -20,6 +20,13 @@
 # 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.
+#
+# 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
+# the label was added (change detected on that branch) or skipped, and why. The
+# 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.
 
 name: Backport Auto Label
 
@@ -67,9 +74,10 @@ jobs:
           # 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 is sent by the bot
-          # (GITHUB_TOKEN, via BOT_TOKEN below) so it is attributed to
-          # github-actions[bot] rather than the PAT owner; it needs no trigger.
+          # 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 || "[]");
@@ -81,9 +89,47 @@ jobs:
             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.
+            const REPORT_MARKER = "<!-- backport-auto-label-report -->";
+            async function upsertReport(bodyMarkdown) {
+              const body = `${REPORT_MARKER}\n${bodyMarkdown}`;
+              const comments = await github.paginate(
+                github.rest.issues.listComments,
+                { owner, repo, issue_number: pr.number, per_page: 100 }
+              );
+              const existing = comments.find((c) =>
+                (c.body || "").includes(REPORT_MARKER)
+              );
+              try {
+                if (existing) {
+                  await bot.rest.issues.updateComment({
+                    owner, repo, comment_id: existing.id, body,
+                  });
+                  core.info(`Updated backport report comment ${existing.id}.`);
+                } else {
+                  await bot.rest.issues.createComment({
+                    owner, repo, issue_number: pr.number, body,
+                  });
+                  core.info("Created backport report comment.");
+                }
+              } catch (e) {
+                core.warning(`Could not upsert report comment (status 
${e.status ?? "?"}): ${e.message}`);
+              }
+            }
 
             // Conventional-commit `fix:` (optional scope, optional `!`). Other
-            // types (feat/chore/docs/...) are not auto-backported.
+            // 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;
@@ -96,6 +142,13 @@ jobs:
             // 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.`);
+              await upsertReport(
+                "### Backport auto-label report\n\n" +
+                "This `fix:` PR carries **`no-backport-needed`**, so no 
release " +
+                "branch was auto-labeled. Remove that label if this fix should 
" +
+                "be backported after all.\n\n" +
+                `_[Auto-label run](${runUrl})._`
+              );
               return;
             }
 
@@ -107,25 +160,29 @@ jobs:
             // 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.
-            async function featureAbsentOnBranch(prNumber, branch) {
+            // 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 false; // pure-add fix 
applies cleanly
+              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 false; // a modified file exists on the 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 false;
+                  return { absent: false, checked: preexisting };
                 }
               }
-              return true; // every modified file is absent from the branch
+              return { absent: true, checked: preexisting }; // every modified 
file absent
             }
 
             // Labels the author (or anyone) explicitly removed. Re-adding one
@@ -157,28 +214,59 @@ jobs:
               }
             }
 
+            // 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 auto-labeled. An 
inactive
-              // branch stays a valid manual target (label it by hand and the
-              // apply-check / post-merge backport still run) — just not by
-              // default.
+              // 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;
               }
-              if (await featureAbsentOnBranch(pr.number, label)) {
+
+              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;
               }
 
@@ -191,12 +279,12 @@ jobs:
               });
               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 {
-                  // Send the review request as github-actions[bot] 
(BOT_TOKEN),
-                  // not the PAT owner — it needs no downstream trigger, so it
-                  // does not have to use the label PAT.
-                  const bot = 
require("@actions/github").getOctokit(process.env.BOT_TOKEN);
                   await bot.rest.pulls.requestReviewers({
                     owner,
                     repo,
@@ -204,6 +292,7 @@ jobs:
                     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}`
@@ -212,4 +301,27 @@ jobs:
               } else if (manager === author) {
                 core.info(`Release manager ${manager} is the author; skipping 
review request.`);
               }
+
+              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
+              .map((r) => `| ${emoji[r.status] || ""} \`${r.branch}\` | 
${r.note} |`)
+              .join("\n");
+            await upsertReport(
+              "### Backport auto-label report\n\n" +
+              "This `fix:` PR was checked against each actively-supported " +
+              "release branch. `release/*` labels drive the post-merge " +
+              "backport, so add or remove one to change where this fix 
lands.\n\n" +
+              "| Release branch | Analysis |\n| --- | --- |\n" +
+              rows +
+              "\n\n" +
+              `_[Auto-label run](${runUrl})._`
+            );

Reply via email to