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 ee41d9a2a6 ci: move backport preflight out of Required Checks and
report conflicts as neutral (#6959)
ee41d9a2a6 is described below
commit ee41d9a2a6d977ddc10b8cee0ef1f04da7b3ef31
Author: Yicong Huang <[email protected]>
AuthorDate: Mon Jul 27 19:07:19 2026 -0700
ci: move backport preflight out of Required Checks and report conflicts as
neutral (#6959)
### What changes were proposed in this PR?
Follow-up to #6941, improving the backport CI/PRs so they stop reading
as failures:
- **A backport conflict no longer shows a red X.** Moved backport
preflight/build out of `required-checks.yml` into a dedicated **Backport
Checks** workflow, and report a conflict as a **`neutral`** (grey) check
instead of a failing job. Still advisory, still never blocks merge
(backport was never a required context).
- **Shared precheck.** Extracted `precheck` into a reusable workflow so
the label→stack mapping lives in one place.
- **Nicer auto-opened draft backport PRs:** opened as
`github-actions[bot]`, follow the PR template, title no longer links
back to the original PR (trailing `(#N)` stripped), and the body carries
the original PR's linked issue/discussion.
### Any related issues, documentation, discussions?
Closes #6954. Follow-up to #6941.
### How was this PR tested?
`actionlint` and `node --check` clean on all workflows; the apply-check
shell/jq and the title/related-section logic were exercised locally.
Full behavior runs on a real backport-labeled PR.
### Was this PR authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 4.8)
---
.github/workflows/backport-auto-label.yml | 17 +-
.github/workflows/backport-checks.yml | 140 +++++++++
.github/workflows/backport-publish.yml | 129 +++++++++
.github/workflows/direct-backport-push.yml | 214 +++++++++++---
.../{required-checks.yml => precheck.yml} | 230 +++------------
.github/workflows/required-checks.yml | 315 +--------------------
6 files changed, 491 insertions(+), 554 deletions(-)
diff --git a/.github/workflows/backport-auto-label.yml
b/.github/workflows/backport-auto-label.yml
index f10959b8c4..2e4c22d077 100644
--- a/.github/workflows/backport-auto-label.yml
+++ b/.github/workflows/backport-auto-label.yml
@@ -62,11 +62,14 @@ jobs:
uses: actions/github-script@v9
env:
ENTRIES: ${{ steps.targets.outputs.entries }}
+ BOT_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
- # Add the label with the fine-grained PAT (falling back to the
- # default token) so the resulting `labeled` event retriggers the
- # backport pre-merge check in Required Checks. Labels applied by the
- # default GITHUB_TOKEN do not trigger downstream workflows.
+ # 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.
github-token: ${{ secrets.AUTO_MERGE_TOKEN || secrets.GITHUB_TOKEN }}
script: |
const entries = JSON.parse(process.env.ENTRIES || "[]");
@@ -190,7 +193,11 @@ jobs:
if (manager && manager !== author) {
try {
- await github.rest.pulls.requestReviewers({
+ // 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,
pull_number: pr.number,
diff --git a/.github/workflows/backport-checks.yml
b/.github/workflows/backport-checks.yml
new file mode 100644
index 0000000000..7288928ef3
--- /dev/null
+++ b/.github/workflows/backport-checks.yml
@@ -0,0 +1,140 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# Backport preflight, kept out of Required Checks so a conflicting backport is
+# never a red X on the PR and never reads like a required check failed. For
+# each release/* label:
+# 1. apply-check: a git-only dry-run cherry-pick onto the release branch.
+# 2. backport: for targets that apply cleanly, build the backported tree so
+# "applies but does not compile on the release branch" is caught
pre-merge.
+# Neither job fails on a bad backport — the apply outcome is written to an
+# artifact and the Backport Preflight Publish workflow (workflow_run) turns it
+# into a `backport-preflight (<target>)` check that is `neutral` (grey) on a
+# conflict and `success` on a clean apply. Publishing lives in that companion
+# because this workflow runs on `pull_request`, whose token is read-only on
+# fork PRs (renovate, external contributors) and so cannot create check runs.
+# Post-merge, Direct Backport Push reads the neutral/success check and the
+# backport build legs to decide push-straight vs. open-a-draft-PR.
+name: Backport Checks
+
+on:
+ pull_request:
+ types:
+ - opened
+ - reopened
+ - synchronize
+ - labeled
+ - unlabeled
+
+permissions:
+ checks: write
+ contents: read
+ pull-requests: read
+
+concurrency:
+ group: backport-checks-${{ github.event.pull_request.number }}
+ cancel-in-progress: true
+
+jobs:
+ # Reused so the release/* target list (and the label -> stack mapping the
+ # backport build needs) is derived exactly as the main build derives it.
+ precheck:
+ uses: ./.github/workflows/precheck.yml
+
+ # Fast git-only pre-flight: does the change even cherry-pick onto each
+ # release branch? One job loops the targets (each is seconds), records
+ # applied/conflict per target, and emits the clean-target list so a
+ # conflicting target never spins up the expensive backport build below.
+ apply-check:
+ needs: precheck
+ if: ${{ needs.precheck.outputs.backport_targets != '[]' }}
+ runs-on: ubuntu-latest
+ outputs:
+ buildable: ${{ steps.check.outputs.buildable }}
+ steps:
+ - name: Checkout PR head
+ uses: actions/checkout@v7
+ with:
+ ref: refs/pull/${{ github.event.pull_request.number }}/head
+ fetch-depth: 0
+ - name: Dry-run cherry-pick each target
+ id: check
+ env:
+ TARGETS: ${{ needs.precheck.outputs.backport_targets }}
+ RANGE: ${{ format('{0}..{1}', github.event.pull_request.base.sha,
github.event.pull_request.head.sha) }}
+ PR_NUMBER: ${{ github.event.pull_request.number }}
+ HEAD_SHA: ${{ github.event.pull_request.head.sha }}
+ run: |
+ set -uo pipefail
+ mkdir -p preflight
+ results='[]'
+ buildable='[]'
+ for target in $(echo "$TARGETS" | jq -r '.[]'); do
+ # Clear any leftover cherry-pick from the previous target so the
+ # script's `git checkout -B` is not blocked by an unmerged index.
+ git cherry-pick --abort 2>/dev/null || true
+ git rebase --abort 2>/dev/null || true
+ if bash ./.github/scripts/prepare-backport-checkout.sh "$target"
"$RANGE"; then
+ echo "[preflight] ${target}: applies cleanly"
+ applied=true
+ buildable=$(echo "$buildable" | jq -c --arg t "$target" '. +
[$t]')
+ else
+ echo "[preflight] ${target}: conflicts"
+ applied=false
+ fi
+ results=$(echo "$results" | jq -c --arg t "$target" --argjson a
"$applied" '. + [{target: $t, applied: $a}]')
+ done
+ git cherry-pick --abort 2>/dev/null || true
+ echo "buildable=${buildable}" >> "$GITHUB_OUTPUT"
+ printf '%s\n' "$results" > preflight/results.json
+ jq -nc --argjson pr "$PR_NUMBER" --arg sha "$HEAD_SHA" '{pr: $pr,
head_sha: $sha}' > preflight/meta.json
+ echo "Results: ${results}"
+ echo "Buildable: ${buildable}"
+ - name: Upload preflight results
+ uses: actions/upload-artifact@v4
+ with:
+ name: backport-preflight
+ path: preflight/
+ retention-days: 3
+
+ # Build the backported tree for targets that applied cleanly, so a clean
+ # cherry-pick that nonetheless fails to compile on the release branch is
+ # caught before merge (Direct Backport Push reads these legs to decide
+ # push-straight vs. open-a-draft-PR). Same stack selection as the main build.
+ backport:
+ needs: [precheck, apply-check]
+ if: ${{ needs.apply-check.outputs.buildable != '' &&
needs.apply-check.outputs.buildable != '[]' }}
+ strategy:
+ fail-fast: false
+ matrix:
+ target: ${{ fromJson(needs.apply-check.outputs.buildable) }}
+ uses: ./.github/workflows/build.yml
+ with:
+ checkout_ref: refs/pull/${{ github.event.pull_request.number }}/head
+ backport_target_branch: ${{ matrix.target }}
+ backport_commit_range: ${{ format('{0}..{1}',
github.event.pull_request.base.sha, github.event.pull_request.head.sha) }}
+ job_name_suffix: ""
+ run_frontend: ${{ needs.precheck.outputs.run_frontend == 'true' }}
+ run_amber: ${{ needs.precheck.outputs.run_amber == 'true' }}
+ run_amber_integration: ${{ needs.precheck.outputs.run_amber_integration
== 'true' }}
+ run_platform: ${{ needs.precheck.outputs.run_platform == 'true' }}
+ run_platform_integration: ${{
needs.precheck.outputs.run_platform_integration == 'true' }}
+ run_pyamber: ${{ needs.precheck.outputs.run_pyamber == 'true' }}
+ run_agent_service: ${{ needs.precheck.outputs.run_agent_service ==
'true' }}
+ run_infra: ${{ needs.precheck.outputs.run_infra == 'true' }}
+ run_pyright_language_service: ${{
needs.precheck.outputs.run_pyright_language_service == 'true' }}
+ secrets: inherit
diff --git a/.github/workflows/backport-publish.yml
b/.github/workflows/backport-publish.yml
new file mode 100644
index 0000000000..fde3467d64
--- /dev/null
+++ b/.github/workflows/backport-publish.yml
@@ -0,0 +1,129 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# Companion to Backport Checks. That workflow runs on `pull_request`, whose
+# token is read-only on fork PRs and so cannot create check runs; this one runs
+# on `workflow_run` in the base-repo context with `checks: write`, so it can.
+# It reads the preflight artifact and publishes one `backport-preflight
+# (<target>)` check per target: `success` when the change cherry-picks cleanly
+# onto that release branch, `neutral` (grey — never a red X, never a merge
+# blocker) when it conflicts. A conflicting backport is expected and handled
+# after merge by Direct Backport Push, which opens a draft backport PR.
+name: Backport Preflight Publish
+
+on:
+ workflow_run:
+ workflows:
+ - Backport Checks
+ types:
+ - completed
+
+permissions:
+ actions: read
+ checks: write
+
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Download preflight artifact
+ id: download
+ continue-on-error: true
+ uses: actions/download-artifact@v4
+ with:
+ name: backport-preflight
+ path: preflight
+ run-id: ${{ github.event.workflow_run.id }}
+ github-token: ${{ github.token }}
+
+ - name: Publish neutral/success preflight checks
+ uses: actions/github-script@v9
+ with:
+ script: |
+ const fs = require("fs");
+ const { owner, repo } = context.repo;
+
+ // The artifact is absent when Backport Checks requested no targets
+ // (a non-backport PR, or every release/* label removed). Fall back
+ // to the workflow_run's head SHA and an empty result set so the
+ // stale-check cleanup below still runs.
+ let results = [];
+ let headSha = context.payload.workflow_run.head_sha;
+ try {
+ results = JSON.parse(fs.readFileSync("preflight/results.json",
"utf8"));
+ } catch (e) {
+ core.info(`No preflight results artifact (${e.code ||
e.message}); nothing to publish.`);
+ }
+ try {
+ const meta = JSON.parse(fs.readFileSync("preflight/meta.json",
"utf8"));
+ if (meta.head_sha) headSha = meta.head_sha;
+ } catch (e) {
+ core.info("No preflight meta; using workflow_run head SHA.");
+ }
+ if (!headSha) {
+ core.warning("No head SHA resolved; skipping.");
+ return;
+ }
+
+ for (const r of results) {
+ const applied = r.applied === true;
+ await github.rest.checks.create({
+ owner,
+ repo,
+ name: `backport-preflight (${r.target})`,
+ head_sha: headSha,
+ status: "completed",
+ conclusion: applied ? "success" : "neutral",
+ output: {
+ title: applied
+ ? `Cherry-picks cleanly onto ${r.target}`
+ : `Conflicts on ${r.target} — needs a manual backport`,
+ summary: applied
+ ? `The change applies cleanly onto \`${r.target}\`. If
this PR merges it is cherry-picked straight to the release branch.`
+ : `The change does not apply cleanly onto \`${r.target}\`.
This is advisory and does not block merging: after merge, Direct Backport Push
opens a draft backport PR for manual conflict resolution. Remove the
\`${r.target}\` label to decline the backport.`,
+ },
+ });
+ core.info(`Published backport-preflight (${r.target}) =
${applied ? "success" : "neutral"}.`);
+ }
+
+ // Cancel stale preflight checks for targets no longer requested
+ // (e.g. a release/* label was removed): a leftover grey/green mark
+ // for a target that is gone is misleading.
+ const current = new Set(results.map((r) => r.target));
+ const checks = await
github.paginate(github.rest.checks.listForRef, {
+ owner,
+ repo,
+ ref: headSha,
+ per_page: 100,
+ });
+ for (const check of checks) {
+ const m = check.name.match(/^backport-preflight \((.+)\)$/);
+ if (!m || current.has(m[1])) continue;
+ if (check.status === "completed" && check.conclusion ===
"cancelled") continue;
+ try {
+ await github.rest.checks.update({
+ owner,
+ repo,
+ check_run_id: check.id,
+ status: "completed",
+ conclusion: "cancelled",
+ });
+ core.info(`Cancelled stale ${check.name}.`);
+ } catch (e) {
+ core.warning(`Failed to cancel ${check.name}: ${e.message}`);
+ }
+ }
diff --git a/.github/workflows/direct-backport-push.yml
b/.github/workflows/direct-backport-push.yml
index 58031e26e1..2ce8815e70 100644
--- a/.github/workflows/direct-backport-push.yml
+++ b/.github/workflows/direct-backport-push.yml
@@ -23,6 +23,7 @@ on:
permissions:
actions: read
+ checks: read
contents: write
issues: write
pull-requests: write
@@ -147,33 +148,53 @@ jobs:
return null;
}
- // Collect every job across the PR head's Required Checks runs so
- // both the pre-flight apply-check and the full backport build can
- // be inspected per target.
+ // Collect the backport build legs for the PR head. They run in
+ // Backport Checks now; during rollout, older PRs still have their
+ // legs (and the old apply-check job) in Required Checks, so gather
+ // jobs from both workflows.
async function jobsForPr(pullRequest) {
- const buildRuns = await github.paginate(
- github.rest.actions.listWorkflowRuns,
- {
+ const workflowFiles = ["backport-checks.yml",
"required-checks.yml"];
+ const allJobs = [];
+ for (const wf of workflowFiles) {
+ let runs = [];
+ try {
+ runs = await
github.paginate(github.rest.actions.listWorkflowRuns, {
+ owner,
+ repo,
+ workflow_id: wf,
+ head_sha: pullRequest.head.sha,
+ per_page: 100,
+ });
+ } catch (e) {
+ core.info(`No ${wf} runs for ${pullRequest.head.sha}
(${e.status ?? "?"}).`);
+ continue;
+ }
+ for (const run of runs) {
+ const jobs = await github.paginate(
+ github.rest.actions.listJobsForWorkflowRun,
+ { owner, repo, run_id: run.id, per_page: 100 }
+ );
+ allJobs.push(...jobs);
+ }
+ }
+ return allJobs;
+ }
+
+ // The apply-check outcome is now a published check run,
+ // `backport-preflight (<target>)`: success = clean, neutral =
+ // conflict. Read the PR head's check runs to find them.
+ async function checkRunsForPr(pullRequest) {
+ try {
+ return await github.paginate(github.rest.checks.listForRef, {
owner,
repo,
- workflow_id: "required-checks.yml",
- head_sha: pullRequest.head.sha,
+ ref: pullRequest.head.sha,
per_page: 100,
- }
- );
- if (buildRuns.length === 0) {
- core.warning(`No Required Checks runs found for
${pullRequest.head.sha}.`);
+ });
+ } catch (e) {
+ core.warning(`listForRef ${pullRequest.head.sha} failed:
${e.message}`);
return [];
}
- const allJobs = [];
- for (const run of buildRuns) {
- const jobs = await github.paginate(
- github.rest.actions.listJobsForWorkflowRun,
- { owner, repo, run_id: run.id, per_page: 100 }
- );
- allJobs.push(...jobs);
- }
- return allJobs;
}
const isDone = (job) => job.status === "completed";
@@ -193,10 +214,17 @@ jobs:
// job could push a conflict or open a spurious PR, so we
// leave it (the scheduled reconciliation, if any, or a
// re-push will pick it up once checks are done).
- function classifyTargets(allJobs, requestedTargets) {
+ function classifyTargets(allJobs, checkRuns, requestedTargets) {
const push = [];
const pr = [];
for (const target of requestedTargets) {
+ // Preferred apply signal: the published preflight check
+ // (success = clean, neutral = conflict).
+ const applyCheck = checkRuns.find(
+ (c) => c.name === `backport-preflight (${target})`
+ );
+ // Rollout fallback: PRs whose last run predates the split
still
+ // expose the old red/green apply-check job in Required Checks.
const applyJob = allJobs.find(
(job) => job.name === `backport-apply-check (${target})`
);
@@ -204,7 +232,16 @@ jobs:
job.name.startsWith(`backport (${target}) / `)
);
- if (applyJob && isDone(applyJob) && !isGreen(applyJob)) {
+ // Resolve the apply signal into clean | conflict | unknown.
+ let apply = "unknown";
+ if (applyCheck && applyCheck.status === "completed") {
+ if (applyCheck.conclusion === "success") apply = "clean";
+ else if (applyCheck.conclusion === "neutral") apply =
"conflict";
+ } else if (applyJob && isDone(applyJob)) {
+ apply = isGreen(applyJob) ? "clean" : "conflict";
+ }
+
+ if (apply === "conflict") {
pr.push(target); // cherry-pick conflicts
continue;
}
@@ -213,12 +250,11 @@ jobs:
continue;
}
- const applyOk = applyJob && isDone(applyJob) &&
isGreen(applyJob);
const anyBuildDone = buildJobs.some(isDone);
- // Normal path: apply-check finished green. The `!applyJob`
- // clause keeps older runs (before apply-check existed) working
- // off the build legs alone during rollout.
- if (applyOk || (!applyJob && anyBuildDone)) {
+ // Normal path: apply finished clean. The `unknown` +
build-legs
+ // clause keeps older runs (before the apply signal existed)
+ // working off the build legs alone during rollout.
+ if (apply === "clean" || (apply === "unknown" &&
anyBuildDone)) {
push.push(target);
} else {
core.warning(`PR: no completed backport signal for
${target}; skipping.`);
@@ -272,7 +308,8 @@ jobs:
}
const allJobs = await jobsForPr(pullRequest);
- const { push, pr } = classifyTargets(allJobs, requestedTargets);
+ const checkRuns = await checkRunsForPr(pullRequest);
+ const { push, pr } = classifyTargets(allJobs, checkRuns,
requestedTargets);
core.info(
`PR #${pullRequest.number}: push=[${push.join(", ")}]
pr=[${pr.join(", ")}]`
);
@@ -788,7 +825,12 @@ jobs:
SUBJECT: ${{ steps.branch.outputs.subject }}
FEATURE_ABSENT: ${{ steps.branch.outputs.feature_absent }}
with:
- github-token: ${{ secrets.AUTO_MERGE_TOKEN || secrets.GITHUB_TOKEN }}
+ # Open the draft as github-actions[bot], not the PAT owner, by using
+ # the default GITHUB_TOKEN. Trade-off: a GITHUB_TOKEN-opened PR does
+ # not trigger pull_request CI — acceptable because this is a draft
for
+ # manual conflict/build resolution, so CI fires once the human pushes
+ # their fix to the branch.
+ github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const {
MERGE_SHA, TARGET_BRANCH, PR_NUMBER, MANAGER,
@@ -834,8 +876,12 @@ jobs:
// the backport PR reads e.g. `fix(amber, v1.2): ...`. Falls back
to
// suffixing the version if the subject is not conventional.
function backportTitle(subject, version) {
- const m = subject.match(/^(\w+)(?:\(([^)]*)\))?(!)?:\s*(.*)$/);
- if (!m) return `${subject} (${version})`;
+ // Drop the trailing "(#N)" GitHub appends to the squash subject
+ // so the backport title does not render as a link back to the
+ // original PR.
+ const clean = subject.replace(/\s*\(#\d+\)\s*$/, "");
+ const m = clean.match(/^(\w+)(?:\(([^)]*)\))?(!)?:\s*(.*)$/);
+ if (!m) return `${clean} (${version})`;
const [, type, scope, bang, rest] = m;
const newScope = scope ? `${scope}, ${version}` : version;
return `${type}(${newScope})${bang || ""}: ${rest}`;
@@ -855,17 +901,84 @@ jobs:
const conflictList = CONFLICT_FILES.trim()
? CONFLICT_FILES.trim().split(/\s+/).map((f) => `-
\`${f}\``).join("\n")
: "";
+ const statusLine = hadConflict
+ ? `The cherry-pick **conflicted** and was committed with
conflict markers. ` +
+ `Resolve the conflicts on this branch, then mark this PR
ready for review.`
+ : `The cherry-pick applied cleanly but the backported tree
failed its ` +
+ `pre-merge build. Fix the build on this branch, then mark
this PR ready for review.`;
+
+ // Carry the original PR's linked issues (its GitHub-linked
+ // closing references) and any discussion / mailing-list links
+ // into the backport so the release-branch reviewer has the same
+ // context. Plain `#N` mentions, never `Closes` — the backport
+ // must not re-close an issue the original PR already resolved.
+ let relatedSection = `Backport of #${prNumber}.`;
+ try {
+ const linked = await github.graphql(
+ `query ($owner: String!, $repo: String!, $num: Int!) {
+ repository(owner: $owner, name: $repo) {
+ pullRequest(number: $num) {
+ body
+ closingIssuesReferences(first: 20) { nodes { number } }
+ }
+ }
+ }`,
+ { owner, repo, num: prNumber }
+ );
+ const original = linked.repository.pullRequest;
+ const bodyText = original.body || "";
+ const issues = (original.closingIssuesReferences?.nodes ||
[]).map(
+ (n) => n.number
+ );
+ const discussions = [
+ ...bodyText.matchAll(
+
/https:\/\/(?:github\.com\/[^\s)]+\/discussions\/\d+|lists\.apache\.org\/[^\s)]+)/g
+ ),
+ ].map((m) => m[0]);
+ // Security dependency PRs (e.g. Renovate) usually link no
issue;
+ // fall back to the CVE / GHSA advisories named in the body so
+ // the backport still points at what it fixes.
+ const cves = [
+ ...new
Set([...bodyText.matchAll(/\bCVE-\d{4}-\d{4,7}\b/g)].map((m) => m[0])),
+ ];
+ const ghsas = [
+ ...new Set(
+
[...bodyText.matchAll(/\bGHSA(?:-[a-z0-9]{4}){3}\b/gi)].map((m) => m[0])
+ ),
+ ];
+ const advisories = cves.length ? cves : ghsas;
+ const parts = [`Backport of #${prNumber}.`];
+ if (issues.length) {
+ parts.push(`Originally linked ${issues.map((n) =>
`#${n}`).join(", ")}.`);
+ } else if (advisories.length) {
+ parts.push(`Fixes ${advisories.join(", ")} (see #${prNumber}
for details).`);
+ }
+ parts.push(...discussions);
+ relatedSection = parts.join(" ");
+ } catch (e) {
+ core.warning(`Could not read linked issues from #${prNumber}:
${e.message}`);
+ }
+
const body = [
- `Automated backport of #${prNumber} to \`${TARGET_BRANCH}\`.`,
+ `### What changes were proposed in this PR?`,
``,
- hadConflict
- ? `⚠️ The cherry-pick **conflicted** and was committed with
conflict markers. ` +
- `Resolve the conflicts on this branch, then mark the PR
ready for review.`
- : `The cherry-pick applied cleanly but the backported tree
failed its ` +
- `pre-merge build. Fix the build on this branch, then mark
the PR ready for review.`,
- conflictList ? `\n**Conflicting files:**\n${conflictList}` :
``,
+ `Automated backport of #${prNumber} to \`${TARGET_BRANCH}\`.`,
``,
`Source: ${MERGE_SHA} · [automation run](${runUrl})`,
+ ``,
+ `### Any related issues, documentation, discussions?`,
+ ``,
+ relatedSection,
+ ``,
+ `### How was this PR tested?`,
+ ``,
+ `Release-branch CI runs on this branch once the ` +
+ `${hadConflict ? "conflicts are resolved" : "build is
fixed"} and ` +
+ `this PR is marked ready for review.`,
+ ``,
+ `### Was this PR authored or co-authored using generative AI
tooling?`,
+ ``,
+ `No.`,
].join("\n");
pr = (await github.rest.pulls.create({
@@ -873,6 +986,19 @@ jobs:
title, body, draft: true,
})).data;
core.info(`Opened draft backport PR #${pr.number}.`);
+
+ // Post the how-to-finish instructions as a comment on the new
+ // backport PR (kept out of the templated body).
+ const statusComment = conflictList
+ ? `${statusLine}\n\n**Conflicting files:**\n${conflictList}`
+ : statusLine;
+ try {
+ await github.rest.issues.createComment({
+ owner, repo, issue_number: pr.number, body: statusComment,
+ });
+ } catch (e) {
+ core.warning(`Could not comment on backport PR #${pr.number}:
${e.message}`);
+ }
}
// Assign the original author (best-effort: external authors may
not
@@ -906,13 +1032,17 @@ jobs:
// Link the backport PR back from the original PR so it is not
lost.
const prUrl =
`${context.serverUrl}/${owner}/${repo}/pull/${pr.number}`;
+ const reason = hadConflict
+ ? "the cherry-pick conflicts"
+ : "the backported tree failed its pre-merge build";
try {
await github.rest.issues.createComment({
owner, repo, issue_number: prNumber,
body:
- `Backport to \`${TARGET_BRANCH}\` needs manual work — opened
` +
- `draft PR #${pr.number} (${prUrl})` +
- (author ? `, assigned to @${author}` : "") + `.`,
+ `Backport PR opened: draft #${pr.number} (${prUrl}) to ` +
+ `\`${TARGET_BRANCH}\`` +
+ (author ? `, assigned to @${author}` : "") +
+ ` — needs manual work because ${reason}.`,
});
} catch (e) {
core.warning(`Could not comment on #${prNumber}: ${e.message}`);
diff --git a/.github/workflows/required-checks.yml
b/.github/workflows/precheck.yml
similarity index 55%
copy from .github/workflows/required-checks.yml
copy to .github/workflows/precheck.yml
index 6194d621cd..c98589060b 100644
--- a/.github/workflows/required-checks.yml
+++ b/.github/workflows/precheck.yml
@@ -15,47 +15,45 @@
# specific language governing permissions and limitations
# under the License.
-name: Required Checks
+# Reusable pre-flight that both Required Checks (main build) and Backport
+# Checks (backport preflight/build) call, so the label -> stack mapping and
+# the labeler-wait live in exactly one place. On PR events it waits for the
+# Pull Request Labeler to finish, then maps the PR's labels to the build
+# stacks each requires and to the release/* backport targets. Non-PR events
+# (push / merge_group / workflow_dispatch) run every stack and request no
+# backports.
+name: Precheck
on:
- push:
- branches:
- - 'ci-enable/**'
- - 'main'
- - 'release/**'
- pull_request:
- types:
- - opened
- - reopened
- - synchronize
- - labeled
- - unlabeled
- merge_group:
- workflow_dispatch:
+ workflow_call:
+ outputs:
+ run_frontend:
+ value: ${{ jobs.decide.outputs.run_frontend }}
+ run_amber:
+ value: ${{ jobs.decide.outputs.run_amber }}
+ run_amber_integration:
+ value: ${{ jobs.decide.outputs.run_amber_integration }}
+ run_platform:
+ value: ${{ jobs.decide.outputs.run_platform }}
+ run_platform_integration:
+ value: ${{ jobs.decide.outputs.run_platform_integration }}
+ run_pyamber:
+ value: ${{ jobs.decide.outputs.run_pyamber }}
+ run_agent_service:
+ value: ${{ jobs.decide.outputs.run_agent_service }}
+ run_infra:
+ value: ${{ jobs.decide.outputs.run_infra }}
+ run_pyright_language_service:
+ value: ${{ jobs.decide.outputs.run_pyright_language_service }}
+ backport_targets:
+ value: ${{ jobs.decide.outputs.backport_targets }}
permissions:
- checks: write
- contents: read
+ checks: read
pull-requests: read
-concurrency:
- group: ${{ github.workflow }}-${{ github.ref }}
- cancel-in-progress: ${{ github.ref != 'refs/heads/main' &&
!startsWith(github.ref, 'refs/heads/release/') }}
-
jobs:
- # Precheck decides which downstream jobs run for this event:
- # - On PR events, wait for the Pull Request Labeler workflow to finish so
- # the labels it applies (frontend, docs, infra, ...) are available, then
- # gate run_* outputs on those labels.
- # - run_frontend / run_amber / run_platform / run_pyamber /
- # run_agent_service: gate the main build stacks. Each labeler-applied
- # label maps to the stacks it requires (LABEL_STACKS below); the run
- # set is the union across all PR labels. Empty union (e.g. docs-only
- # PRs) skips every stack. Push and workflow_dispatch
- # events run every stack.
- # - backport_targets: JSON array of release/* labels currently on the PR.
- # Drives the backport matrix; empty array means no backport runs.
- precheck:
+ decide:
name: Precheck
runs-on: ubuntu-latest
outputs:
@@ -231,167 +229,3 @@ jobs:
core.info(`Backport targets: ${targets.join(", ")}`);
}
core.setOutput("backport_targets", JSON.stringify(targets));
-
- cleanup-stale-backport:
- if: ${{ github.event_name == 'pull_request' && github.event.action ==
'unlabeled' && startsWith(github.event.label.name, 'release/') }}
- runs-on: ubuntu-latest
- steps:
- - name: Cancel obsolete backport check_runs for the removed target
- uses: actions/github-script@v9
- with:
- script: |
- const { owner, repo } = context.repo;
- const target = context.payload.label.name;
- const headSha = context.payload.pull_request.head.sha;
- const prefix = `backport (${target}) `;
-
- const checks = await github.paginate(
- github.rest.checks.listForRef,
- { owner, repo, ref: headSha, per_page: 100 }
- );
-
- for (const check of checks) {
- if (!check.name.startsWith(prefix)) continue;
- if (check.status === "completed" && check.conclusion ===
"cancelled") continue;
- try {
- await github.rest.checks.update({
- owner,
- repo,
- check_run_id: check.id,
- status: "completed",
- conclusion: "cancelled",
- });
- core.info(`Cancelled check ${check.name}`);
- } catch (e) {
- core.warning(`Failed to update check ${check.id}
(${check.name}): ${e.message}`);
- }
- }
-
- build:
- needs: precheck
- uses: ./.github/workflows/build.yml
- with:
- run_frontend: ${{ needs.precheck.outputs.run_frontend == 'true' }}
- run_amber: ${{ needs.precheck.outputs.run_amber == 'true' }}
- run_amber_integration: ${{ needs.precheck.outputs.run_amber_integration
== 'true' }}
- run_platform: ${{ needs.precheck.outputs.run_platform == 'true' }}
- run_platform_integration: ${{
needs.precheck.outputs.run_platform_integration == 'true' }}
- run_pyamber: ${{ needs.precheck.outputs.run_pyamber == 'true' }}
- run_agent_service: ${{ needs.precheck.outputs.run_agent_service ==
'true' }}
- run_infra: ${{ needs.precheck.outputs.run_infra == 'true' }}
- run_pyright_language_service: ${{
needs.precheck.outputs.run_pyright_language_service == 'true' }}
- secrets: inherit
-
- # Fast pre-flight: does the change even cherry-pick onto each release branch?
- # This is a git-only job (no build), so a conflict is reported in seconds and
- # gates the expensive backport build below — a conflicting target never spins
- # up the full stack matrix. Post-merge, Direct Backport Push reads this job's
- # result as the authoritative "conflicts / applies" signal.
- backport-apply-check:
- needs: precheck
- if: ${{ needs.precheck.outputs.backport_targets != '[]' }}
- strategy:
- fail-fast: false
- matrix:
- target: ${{ fromJson(needs.precheck.outputs.backport_targets) }}
- runs-on: ubuntu-latest
- steps:
- - name: Checkout PR head
- uses: actions/checkout@v7
- with:
- ref: refs/pull/${{ github.event.pull_request.number }}/head
- fetch-depth: 0
- - name: Dry-run cherry-pick onto ${{ matrix.target }}
- run: |
- bash ./.github/scripts/prepare-backport-checkout.sh \
- "${{ matrix.target }}" \
- "${{ format('{0}..{1}', github.event.pull_request.base.sha,
github.event.pull_request.head.sha) }}"
-
- # Collect the targets whose pre-flight cherry-pick succeeded. always() so a
- # conflict on one target still lets the others build; an all-conflict run
- # yields an empty list and skips the backport build entirely.
- backport-buildable:
- needs: [precheck, backport-apply-check]
- if: ${{ always() && needs.precheck.outputs.backport_targets != '[]' }}
- runs-on: ubuntu-latest
- outputs:
- targets: ${{ steps.collect.outputs.targets }}
- steps:
- - name: Collect targets that applied cleanly
- id: collect
- uses: actions/github-script@v9
- env:
- REQUESTED: ${{ needs.precheck.outputs.backport_targets }}
- with:
- script: |
- const requested = JSON.parse(process.env.REQUESTED || "[]");
- const { owner, repo } = context.repo;
- const jobs = await github.paginate(
- github.rest.actions.listJobsForWorkflowRun,
- { owner, repo, run_id: context.runId, per_page: 100 }
- );
- const buildable = requested.filter((target) => {
- const job = jobs.find(
- (j) => j.name === `backport-apply-check (${target})`
- );
- return job && job.conclusion === "success";
- });
- core.info(`Buildable backport targets:
${JSON.stringify(buildable)}`);
- core.setOutput("targets", JSON.stringify(buildable));
-
- backport:
- needs: [precheck, backport-buildable]
- if: ${{ needs.backport-buildable.outputs.targets != '' &&
needs.backport-buildable.outputs.targets != '[]' }}
- strategy:
- fail-fast: false
- matrix:
- target: ${{ fromJson(needs.backport-buildable.outputs.targets) }}
- uses: ./.github/workflows/build.yml
- with:
- checkout_ref: refs/pull/${{ github.event.pull_request.number }}/head
- backport_target_branch: ${{ matrix.target }}
- backport_commit_range: ${{ format('{0}..{1}',
github.event.pull_request.base.sha, github.event.pull_request.head.sha) }}
- job_name_suffix: ""
- run_frontend: ${{ needs.precheck.outputs.run_frontend == 'true' }}
- run_amber: ${{ needs.precheck.outputs.run_amber == 'true' }}
- run_amber_integration: ${{ needs.precheck.outputs.run_amber_integration
== 'true' }}
- run_platform: ${{ needs.precheck.outputs.run_platform == 'true' }}
- run_platform_integration: ${{
needs.precheck.outputs.run_platform_integration == 'true' }}
- run_pyamber: ${{ needs.precheck.outputs.run_pyamber == 'true' }}
- run_agent_service: ${{ needs.precheck.outputs.run_agent_service ==
'true' }}
- run_infra: ${{ needs.precheck.outputs.run_infra == 'true' }}
- run_pyright_language_service: ${{
needs.precheck.outputs.run_pyright_language_service == 'true' }}
- secrets: inherit
-
- required-checks:
- # Do not rename this job — its display name is referenced in .asf.yaml.
- name: Required Checks
- # The backport job is deliberately NOT a dependency here: it is an advisory
- # signal, not a merge gate. A red pre-merge backport (a cherry-pick that
- # conflicts, or a backported tree that fails to build) must not block the
- # PR from landing on main — instead Direct Backport Push opens a draft
- # backport PR for the author after merge. Keeping backport out of `needs`
- # also lets this aggregator report as soon as build finishes.
- needs: [precheck, build]
- if: always()
- runs-on: ubuntu-latest
- steps:
- - name: Verify all required checks succeeded or were skipped
- run: |
- declare -A results=(
- [precheck]="${{ needs.precheck.result }}"
- [build]="${{ needs.build.result }}"
- )
- failed=0
- for job in "${!results[@]}"; do
- r="${results[$job]}"
- echo "${job}: ${r}"
- if [[ "$r" != "success" && "$r" != "skipped" ]]; then
- failed=1
- fi
- done
- if (( failed )); then
- echo "::error::One or more required checks did not succeed."
- exit 1
- fi
- echo "All required checks succeeded or were skipped."
diff --git a/.github/workflows/required-checks.yml
b/.github/workflows/required-checks.yml
index 6194d621cd..f2ee2efb89 100644
--- a/.github/workflows/required-checks.yml
+++ b/.github/workflows/required-checks.yml
@@ -43,229 +43,13 @@ concurrency:
cancel-in-progress: ${{ github.ref != 'refs/heads/main' &&
!startsWith(github.ref, 'refs/heads/release/') }}
jobs:
- # Precheck decides which downstream jobs run for this event:
- # - On PR events, wait for the Pull Request Labeler workflow to finish so
- # the labels it applies (frontend, docs, infra, ...) are available, then
- # gate run_* outputs on those labels.
- # - run_frontend / run_amber / run_platform / run_pyamber /
- # run_agent_service: gate the main build stacks. Each labeler-applied
- # label maps to the stacks it requires (LABEL_STACKS below); the run
- # set is the union across all PR labels. Empty union (e.g. docs-only
- # PRs) skips every stack. Push and workflow_dispatch
- # events run every stack.
- # - backport_targets: JSON array of release/* labels currently on the PR.
- # Drives the backport matrix; empty array means no backport runs.
+ # Decide which build stacks this event needs (label union on PR events; all
+ # stacks on push/merge_group/dispatch). Shared with Backport Checks via the
+ # reusable precheck workflow so the label -> stack mapping lives in one place
+ # (.github/workflows/precheck.yml). The backport_targets output is unused
+ # here — backport preflight/build moved to the Backport Checks workflow.
precheck:
- name: Precheck
- runs-on: ubuntu-latest
- outputs:
- run_frontend: ${{ steps.decide.outputs.run_frontend }}
- run_amber: ${{ steps.decide.outputs.run_amber }}
- run_amber_integration: ${{ steps.decide.outputs.run_amber_integration }}
- run_platform: ${{ steps.decide.outputs.run_platform }}
- run_platform_integration: ${{
steps.decide.outputs.run_platform_integration }}
- run_pyamber: ${{ steps.decide.outputs.run_pyamber }}
- run_agent_service: ${{ steps.decide.outputs.run_agent_service }}
- run_infra: ${{ steps.decide.outputs.run_infra }}
- run_pyright_language_service: ${{
steps.decide.outputs.run_pyright_language_service }}
- backport_targets: ${{ steps.decide.outputs.backport_targets }}
- steps:
- - name: Wait for Pull Request Labeler
- if: github.event_name == 'pull_request'
- uses: actions/github-script@v9
- with:
- script: |
- const ref = context.payload.pull_request.head.sha;
- const maxAttempts = 30;
- for (let i = 0; i < maxAttempts; i++) {
- const { data } = await github.rest.checks.listForRef({
- owner: context.repo.owner,
- repo: context.repo.repo,
- ref,
- check_name: "labeler",
- });
- const check = data.check_runs[0];
- if (check && check.status === "completed") {
- core.info(`labeler ${check.conclusion}`);
- return;
- }
- core.info(`labeler not ready (attempt ${i + 1}/${maxAttempts})`);
- await new Promise((r) => setTimeout(r, 10000));
- }
- core.warning("labeler did not complete within 5 minutes;
proceeding with current labels.");
-
- - name: Decide which jobs to run
- id: decide
- uses: actions/github-script@v9
- with:
- script: |
- const eventName = context.eventName;
- let labels = [];
-
- if (eventName === "pull_request") {
- // Re-fetch labels: the labeler may have just added some.
- const { data: pr } = await github.rest.pulls.get({
- owner: context.repo.owner,
- repo: context.repo.repo,
- pull_number: context.payload.pull_request.number,
- });
- labels = pr.labels.map((l) => l.name);
- core.info(`PR labels: ${labels.join(", ") || "(none)"}`);
- }
-
- // Map each labeler-applied label to the stacks it requires. The
- // run set is the union across all PR labels. Labels not listed
- // here (release/*, feature, fix, refactor) contribute no stacks.
- // dependencies is intentionally a no-op: every dep manifest the
- // labeler matches lives under a component dir and is already
- // covered by that component's label — including
- // pyright-language-service/package.json, kept honest by the
- // `pyright-language-service` component label below.
- //
- // `infra` and `pyright-language-service` are 1:1 self-mapping
- // labels (each fires only its own like-named stack); they are
- // omitted from the illustrative table below to keep it narrow.
- //
- // `platform-integration` mirrors `amber-integration`: a dedicated
- // `platform-integration` label fires only the integration stack
- // (a spec-only change) without the full `platform` job. It is
- // omitted from the table below to keep it narrow. No labeler rule
- // applies it yet — platform has no integration specs (#6047) — so
- // today it is a forward hook (applied manually until then).
- //
- // label | frontend | amber | amber-integ | platform
| pyamber | agent-service
- //
------------------|----------|-------|-------------|----------|---------|--------------
- // frontend | x | | |
| |
- // pyamber | | | x |
| x |
- // engine | | x | x |
| |
- // amber-integration | | | x |
| |
- // platform | | | | x
| |
- // agent-service | | | |
| | x
- // common | | x | x | x
| | (root
- //
scala
- //
build/lint
- //
config)
- // ddl-change | | x | x | x
| |
- // ci | x | x | x | x
| x | x
- // docs / deps / | | | |
| |
- // release/* | | | |
| |
- // * / branch | | | |
| |
- //
- // amber-integration runs the Scala tests tagged
- // @org.apache.texera.amber.tags.IntegrationTest (e2e specs that
- // spawn Python UDF workers). The labeler attaches `pyamber` to
- // any *.py change (including amber/src/{main,test}/python/**),
- // so `engine` does not need to fire the pyamber stack itself —
- // pure-Python amber changes pick up `pyamber` directly. The
- // `amber-integration` label catches *IntegrationSpec.scala
- // edits so a test-only change does not trigger the full
- // Scala-only amber stack.
- const LABEL_STACKS = {
- frontend: ["frontend"],
- pyamber: ["amber-integration", "pyamber"],
- engine: ["amber", "amber-integration"],
- "amber-integration": ["amber-integration"],
- platform: ["platform", "platform-integration"],
- "platform-integration": ["platform-integration"],
- "agent-service": ["agent-service"],
- "pyright-language-service": ["pyright-language-service"],
- common: ["amber", "amber-integration", "platform",
"platform-integration"],
- "ddl-change": ["amber", "amber-integration", "platform",
"platform-integration"],
- infra: ["infra"],
- ci: ["frontend", "amber", "amber-integration", "platform",
"platform-integration", "pyamber", "agent-service", "infra",
"pyright-language-service"],
- };
-
- let runFrontend = true;
- let runAmber = true;
- let runAmberIntegration = true;
- let runPlatform = true;
- let runPlatformIntegration = true;
- let runPyamber = true;
- let runAgentService = true;
- let runInfra = true;
- let runPyrightLanguageService = true;
-
- if (eventName === "pull_request") {
- const stacks = new Set();
- for (const label of labels) {
- for (const stack of LABEL_STACKS[label] || []) {
- stacks.add(stack);
- }
- }
- runFrontend = stacks.has("frontend");
- runAmber = stacks.has("amber");
- runAmberIntegration = stacks.has("amber-integration");
- runPlatform = stacks.has("platform");
- runPlatformIntegration = stacks.has("platform-integration");
- runPyamber = stacks.has("pyamber");
- runAgentService = stacks.has("agent-service");
- runInfra = stacks.has("infra");
- runPyrightLanguageService =
stacks.has("pyright-language-service");
- core.info(
- `Stacks selected by label union: ${[...stacks].sort().join(",
") || "(none)"}`
- );
- }
-
- core.setOutput("run_frontend", runFrontend ? "true" : "false");
- core.setOutput("run_amber", runAmber ? "true" : "false");
- core.setOutput("run_amber_integration", runAmberIntegration ?
"true" : "false");
- core.setOutput("run_platform", runPlatform ? "true" : "false");
- core.setOutput("run_platform_integration", runPlatformIntegration
? "true" : "false");
- core.setOutput("run_pyamber", runPyamber ? "true" : "false");
- core.setOutput("run_agent_service", runAgentService ? "true" :
"false");
- core.setOutput("run_infra", runInfra ? "true" : "false");
- core.setOutput("run_pyright_language_service",
runPyrightLanguageService ? "true" : "false");
-
- // Backport targets: all current release/* labels on the PR, unless
- // `no-backport-needed` vetoes backporting entirely (e.g. a fix for
- // a feature that does not exist on any release branch). The veto
- // wins even if release/* labels are also present, so adding it
- // mid-review cancels the backport apply-check/build on the re-run.
- let targets = [...new Set(labels.filter((n) =>
/^release\/.+$/.test(n)))].sort();
- if (labels.includes("no-backport-needed")) {
- core.info("no-backport-needed present; skipping all backport
targets.");
- targets = [];
- } else if (targets.length === 0) {
- core.info("No backport targets on PR.");
- } else {
- core.info(`Backport targets: ${targets.join(", ")}`);
- }
- core.setOutput("backport_targets", JSON.stringify(targets));
-
- cleanup-stale-backport:
- if: ${{ github.event_name == 'pull_request' && github.event.action ==
'unlabeled' && startsWith(github.event.label.name, 'release/') }}
- runs-on: ubuntu-latest
- steps:
- - name: Cancel obsolete backport check_runs for the removed target
- uses: actions/github-script@v9
- with:
- script: |
- const { owner, repo } = context.repo;
- const target = context.payload.label.name;
- const headSha = context.payload.pull_request.head.sha;
- const prefix = `backport (${target}) `;
-
- const checks = await github.paginate(
- github.rest.checks.listForRef,
- { owner, repo, ref: headSha, per_page: 100 }
- );
-
- for (const check of checks) {
- if (!check.name.startsWith(prefix)) continue;
- if (check.status === "completed" && check.conclusion ===
"cancelled") continue;
- try {
- await github.rest.checks.update({
- owner,
- repo,
- check_run_id: check.id,
- status: "completed",
- conclusion: "cancelled",
- });
- core.info(`Cancelled check ${check.name}`);
- } catch (e) {
- core.warning(`Failed to update check ${check.id}
(${check.name}): ${e.message}`);
- }
- }
+ uses: ./.github/workflows/precheck.yml
build:
needs: precheck
@@ -282,96 +66,9 @@ jobs:
run_pyright_language_service: ${{
needs.precheck.outputs.run_pyright_language_service == 'true' }}
secrets: inherit
- # Fast pre-flight: does the change even cherry-pick onto each release branch?
- # This is a git-only job (no build), so a conflict is reported in seconds and
- # gates the expensive backport build below — a conflicting target never spins
- # up the full stack matrix. Post-merge, Direct Backport Push reads this job's
- # result as the authoritative "conflicts / applies" signal.
- backport-apply-check:
- needs: precheck
- if: ${{ needs.precheck.outputs.backport_targets != '[]' }}
- strategy:
- fail-fast: false
- matrix:
- target: ${{ fromJson(needs.precheck.outputs.backport_targets) }}
- runs-on: ubuntu-latest
- steps:
- - name: Checkout PR head
- uses: actions/checkout@v7
- with:
- ref: refs/pull/${{ github.event.pull_request.number }}/head
- fetch-depth: 0
- - name: Dry-run cherry-pick onto ${{ matrix.target }}
- run: |
- bash ./.github/scripts/prepare-backport-checkout.sh \
- "${{ matrix.target }}" \
- "${{ format('{0}..{1}', github.event.pull_request.base.sha,
github.event.pull_request.head.sha) }}"
-
- # Collect the targets whose pre-flight cherry-pick succeeded. always() so a
- # conflict on one target still lets the others build; an all-conflict run
- # yields an empty list and skips the backport build entirely.
- backport-buildable:
- needs: [precheck, backport-apply-check]
- if: ${{ always() && needs.precheck.outputs.backport_targets != '[]' }}
- runs-on: ubuntu-latest
- outputs:
- targets: ${{ steps.collect.outputs.targets }}
- steps:
- - name: Collect targets that applied cleanly
- id: collect
- uses: actions/github-script@v9
- env:
- REQUESTED: ${{ needs.precheck.outputs.backport_targets }}
- with:
- script: |
- const requested = JSON.parse(process.env.REQUESTED || "[]");
- const { owner, repo } = context.repo;
- const jobs = await github.paginate(
- github.rest.actions.listJobsForWorkflowRun,
- { owner, repo, run_id: context.runId, per_page: 100 }
- );
- const buildable = requested.filter((target) => {
- const job = jobs.find(
- (j) => j.name === `backport-apply-check (${target})`
- );
- return job && job.conclusion === "success";
- });
- core.info(`Buildable backport targets:
${JSON.stringify(buildable)}`);
- core.setOutput("targets", JSON.stringify(buildable));
-
- backport:
- needs: [precheck, backport-buildable]
- if: ${{ needs.backport-buildable.outputs.targets != '' &&
needs.backport-buildable.outputs.targets != '[]' }}
- strategy:
- fail-fast: false
- matrix:
- target: ${{ fromJson(needs.backport-buildable.outputs.targets) }}
- uses: ./.github/workflows/build.yml
- with:
- checkout_ref: refs/pull/${{ github.event.pull_request.number }}/head
- backport_target_branch: ${{ matrix.target }}
- backport_commit_range: ${{ format('{0}..{1}',
github.event.pull_request.base.sha, github.event.pull_request.head.sha) }}
- job_name_suffix: ""
- run_frontend: ${{ needs.precheck.outputs.run_frontend == 'true' }}
- run_amber: ${{ needs.precheck.outputs.run_amber == 'true' }}
- run_amber_integration: ${{ needs.precheck.outputs.run_amber_integration
== 'true' }}
- run_platform: ${{ needs.precheck.outputs.run_platform == 'true' }}
- run_platform_integration: ${{
needs.precheck.outputs.run_platform_integration == 'true' }}
- run_pyamber: ${{ needs.precheck.outputs.run_pyamber == 'true' }}
- run_agent_service: ${{ needs.precheck.outputs.run_agent_service ==
'true' }}
- run_infra: ${{ needs.precheck.outputs.run_infra == 'true' }}
- run_pyright_language_service: ${{
needs.precheck.outputs.run_pyright_language_service == 'true' }}
- secrets: inherit
-
required-checks:
# Do not rename this job — its display name is referenced in .asf.yaml.
name: Required Checks
- # The backport job is deliberately NOT a dependency here: it is an advisory
- # signal, not a merge gate. A red pre-merge backport (a cherry-pick that
- # conflicts, or a backported tree that fails to build) must not block the
- # PR from landing on main — instead Direct Backport Push opens a draft
- # backport PR for the author after merge. Keeping backport out of `needs`
- # also lets this aggregator report as soon as build finishes.
needs: [precheck, build]
if: always()
runs-on: ubuntu-latest