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})._` + );
