This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] 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 5040cadf38 ci: report backport auto-label decisions on the PR (#6962)
5040cadf38 is described below
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})._`
+ );