You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@texera.apache.org by gi...@apache.org on 2026/09/21 21:40:11 UTC
(texera) 01/01: ci: open a pull request for a clean backport instead of pushing it (#8626)
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-8626-60a37b1e8495b7d16b1458560ae48d0834f7c513
in repository https://gitbox.apache.org/repos/asf/texera.git
commit 35be7c72005cbe47e7bcf675920f96ed786467a9
Author: Meng Wang <me...@uci.edu>
AuthorDate: Mon Sep 21 21:39:50 2026 +0000
ci: open a pull request for a clean backport instead of pushing it (#8626)
### What changes were proposed in this PR?
`Direct Backport Push` cherry-picks a cleanly-applying fix onto the
release branch and pushes it. Every one of those pushes has been
rejected since 2026-07-24: `release/*` is covered by the Merge Queue
ruleset, which requires a pull request of everyone. #8379 tried to
exempt the Actions app from it; GitHub refuses to create that bypass,
and #8624 reverts it. ASF policy points the same way — an automated
service must not push to a branch subject to official release without
prior authorization from Infrastructure.
Both outcomes now open a pull request. The conflicted one is unchanged:
a draft, assigned to its author. A clean one opens **ready for review
and assigned to nobody**, because there is no code for anyone to write
on it.
What a clean backport still needs is its checks started, and that is the
part worth stating plainly. GitHub creates no workflow run for anything
`GITHUB_TOKEN` does, so a bot-opened pull request has none — and nothing
will arrive on its own:
| action on a pull request with no checks | starts the three required
contexts |
| --- | --- |
| push any commit to the branch | yes (`synchronize`) |
| close and reopen it | yes (`reopened`) |
| mark it ready for review | **no** — none of the three workflows
listens for `ready_for_review` |
| add or remove a label | only `Required Checks` |
| "Re-run all jobs" | no — with no run there is nothing to re-run |
A conflicted backport never had this problem: its author pushes a
resolution, and that push brings CI with it. A clean one has nobody to
push anything. So the comment the conflict path already posts for its
instructions now says, for a clean backport, the one action that works —
and says that marking it ready for review is not it.
That leaves the release manager three ordinary buttons: reopen, approve,
and auto-merge if they would rather not come back when the checks
finish. The approval is not automated and should not be: the `release/*`
label on the original PR records the decision, and this is the look at
the tree that actually lands.
Nothing here depends on a token's pull-request scope, on an Actions
bypass, or on a close/reopen the workflow performs itself. Those are the
paths that can only be proven in production, and that fail quietly when
they are wrong — which is how #8432, #8494 and #8562 were lost.
`push_entries` is now always empty, leaving `push-backports`
unreachable. Removing it is left to a separate change, so that this one
is a behaviour change and that one is a pure deletion.
### Any related issues, documentation, discussions?
Closes #8377. #8378 proposed the same routing with the workflow
performing the close/reopen itself and arming auto-merge; this drops
both in favour of the release manager's own click, and is closed in
favour of this.
### How was this PR tested?
The routing was driven locally against a stubbed `github-script`
environment. With the pre-merge preflight green, both targets come out
as pull-request entries carrying `clean: "true"` and `push_entries`
empty; with it neutral, `clean: "false"`; with no completed signal,
neither target is acted on, as before. Restoring the old
`pushEntries.push` turns that check red, so it is not vacuous. The
workflow parses, and all four inline `github-script` bodies pass `node
--check`.
That a bot-opened pull request starts with no checks is what this
repository already shows: #8584 — bot-opened, one commit, nobody pushed
to it — carries no check runs at all, while #8553, opened the same way,
has the full set after a commit was pushed. That `ready_for_review` does
not start them is in the triggers: `required-checks.yml` lists
`opened`/`reopened`/`synchronize`/`labeled`/`unlabeled`,
`check-header.yml` takes the bare `pull_request:` defaults, and
`lint-pr.yml` lists `opened`/`edited`/`reopened`/`synchronize`.
Not provable before merge: that a human reopen produces the three
contexts on a backport PR. #8619 to #8623 — five backports into
`release/v1.3` opened by hand this week — show that the contexts do
appear and pass on a pull request into a release branch; the reopen path
shares everything with them but the event that starts the run.
### Was this PR authored or co-authored using generative AI tooling?
Yes. Generated-by: Claude Code (claude-opus-5)
---
.github/release-branches.yml | 9 +-
.github/scripts/create-backport-branch.sh | 13 ++-
.github/workflows/backport-checks.yml | 3 +-
.github/workflows/backport-publish.yml | 2 +-
.github/workflows/direct-backport-push.yml | 181 ++++++++++++++++++++++++-----
5 files changed, 169 insertions(+), 39 deletions(-)
diff --git a/.github/release-branches.yml b/.github/release-branches.yml
index ff3e76854a..a092d19f20 100644
--- a/.github/release-branches.yml
+++ b/.github/release-branches.yml
@@ -23,10 +23,11 @@
# - blocks the merge until that manager has approved the PR, or removed the
# label to decline (.github/workflows/backport-approval-check.yml, the
# required `Backport Approvals` check — see `manager` below);
-# - drives the post-merge backport (.github/workflows/direct-backport-push.yml):
-# a green pre-merge backport check cherry-picks straight to the branch, a
-# red one opens a draft backport PR assigned to the author with the manager
-# as reviewer.
+# - drives the post-merge backport (.github/workflows/direct-backport-push.yml),
+# which always opens a pull request against the branch: a green pre-merge
+# backport check opens it ready for review with the manager requested, a
+# red one opens a draft assigned to the author with the manager as
+# reviewer.
#
# The label name equals the branch name (e.g. branch `release/v1.2` <-> label
# `release/v1.2`).
diff --git a/.github/scripts/create-backport-branch.sh b/.github/scripts/create-backport-branch.sh
index 32c8fe5e69..8fda47be17 100755
--- a/.github/scripts/create-backport-branch.sh
+++ b/.github/scripts/create-backport-branch.sh
@@ -16,12 +16,13 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-# Builds and pushes the branch behind an auto-opened backport PR, used when the
-# pre-merge backport check was red so a straight cherry-pick to the release
-# branch is unsafe. The cherry-pick is committed even when it conflicts: the
-# tree carries the conflict markers, and the human resolves them in the PR
-# rather than starting the backport from scratch (the same approach the common
-# backport bots take).
+# Builds and pushes the branch behind an auto-opened backport PR. Every backport
+# goes through one now, whatever the pre-merge backport check said: a release
+# branch takes no direct push, so the check only decides whether the PR opens
+# ready for review or as a draft. The cherry-pick is committed even when it
+# conflicts: the tree carries the conflict markers, and the human resolves them
+# in the PR rather than starting the backport from scratch (the same approach
+# the common backport bots take).
#
# Usage: create-backport-branch.sh <merge-sha> <target-branch> <pr-number>
# Writes to $GITHUB_OUTPUT (or stdout when unset): branch, version,
diff --git a/.github/workflows/backport-checks.yml b/.github/workflows/backport-checks.yml
index 7288928ef3..1244d8f7d2 100644
--- a/.github/workflows/backport-checks.yml
+++ b/.github/workflows/backport-checks.yml
@@ -28,7 +28,8 @@
# 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.
+# backport build legs to decide whether the backport PR it opens is ready for
+# review or a draft for its author to finish.
name: Backport Checks
on:
diff --git a/.github/workflows/backport-publish.yml b/.github/workflows/backport-publish.yml
index fde3467d64..5cd309e685 100644
--- a/.github/workflows/backport-publish.yml
+++ b/.github/workflows/backport-publish.yml
@@ -93,7 +93,7 @@ jobs:
? `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 applies cleanly onto \`${r.target}\`. If this PR merges, a backport PR opens against that branch, ready for review.`
: `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.`,
},
});
diff --git a/.github/workflows/direct-backport-push.yml b/.github/workflows/direct-backport-push.yml
index b67fcc90fc..fe96a37ed4 100644
--- a/.github/workflows/direct-backport-push.yml
+++ b/.github/workflows/direct-backport-push.yml
@@ -273,10 +273,12 @@ jobs:
const commits = await listPushCommits();
core.info(`Push contains ${commits.length} commit(s).`);
- // push_entries: (PR, target) pairs cherry-picked straight to the
- // release branch, in commit order.
- // pr_entries: (PR, target, manager) pairs that get an auto-opened
- // draft backport PR for the author to finish.
+ // pr_entries: (PR, target, manager, clean) pairs that each get an
+ // auto-opened backport PR, in commit order -- ready for review
+ // when the cherry-pick is clean, a draft for its author to finish
+ // when it is not.
+ // push_entries: empty. Kept until the unreachable push-backports
+ // job is removed, so that removal is a pure deletion.
const pushEntries = [];
const prEntries = [];
const seenPrNumbers = new Set();
@@ -314,11 +316,23 @@ jobs:
`PR #${pullRequest.number}: push=[${push.join(", ")}] pr=[${pr.join(", ")}]`
);
+ // Both outcomes open a pull request. Pushing a cherry-pick
+ // straight onto a release branch is rejected by the Merge Queue
+ // ruleset that covers them (#8377), and ASF policy asks for prior
+ // Infrastructure authorization before an automated service writes
+ // a branch subject to official release -- so a backport travels
+ // the way every other change to a release branch travels.
+ //
+ // `clean` only changes what the pull request says and whether it
+ // opens as a draft: a clean cherry-pick needs nobody to touch the
+ // code, a conflicted one is still its author's to finish.
for (const target of push) {
- pushEntries.push({
+ prEntries.push({
pr_number: pullRequest.number,
merge_sha: commit.sha,
target,
+ manager: releaseManagers.get(target) || "",
+ clean: "true",
});
}
for (const target of pr) {
@@ -327,12 +341,17 @@ jobs:
merge_sha: commit.sha,
target,
manager: releaseManagers.get(target) || "",
+ clean: "false",
});
}
}
core.info(`Push entries: ${JSON.stringify(pushEntries)}`);
core.info(`PR entries: ${JSON.stringify(prEntries)}`);
+ // Always empty now: every target opens a pull request. The
+ // push-backports job below is therefore unreachable, and is
+ // removed separately so this change is a behaviour change and
+ // that one is a pure deletion.
core.setOutput("push_entries", JSON.stringify(pushEntries));
core.setOutput("pr_entries", JSON.stringify(prEntries));
core.setOutput("has_push", pushEntries.length > 0 ? "true" : "false");
@@ -796,9 +815,14 @@ jobs:
needs: discover
if: ${{ needs.discover.outputs.has_pr == 'true' }}
runs-on: ubuntu-latest
- name: "open backport PR #${{ matrix.pr_number }} to ${{ matrix.target }}"
+ name: "backport PR #${{ matrix.pr_number }} to ${{ matrix.target }}"
strategy:
fail-fast: false
+ # One at a time, as push-backports did: a single push can carry several
+ # fixes for the same release branch, and each cherry-pick is taken
+ # against that branch as it stands, so preparing them concurrently would
+ # race their branches against each other.
+ max-parallel: 1
matrix:
include: ${{ fromJson(needs.discover.outputs.pr_entries) }}
steps:
@@ -818,7 +842,7 @@ jobs:
run: |
bash ./.github/scripts/create-backport-branch.sh \
"${MERGE_SHA}" "${TARGET_BRANCH}" "${PR_NUMBER}"
- - name: Open draft backport PR
+ - name: Open backport PR
uses: actions/github-script@v9
env:
MERGE_SHA: ${{ matrix.merge_sha }}
@@ -831,22 +855,38 @@ jobs:
CONFLICT_FILES: ${{ steps.branch.outputs.conflict_files }}
SUBJECT: ${{ steps.branch.outputs.subject }}
FEATURE_ABSENT: ${{ steps.branch.outputs.feature_absent }}
+ CLEAN: ${{ matrix.clean }}
with:
- # 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.
+ # Opened as github-actions[bot], not as the PAT owner, by using the
+ # default GITHUB_TOKEN — so nobody is recorded as the author of a
+ # pull request they did not write, and the token owner is never
+ # barred from approving one.
+ #
+ # The cost is that GitHub creates no workflow run for anything
+ # GITHUB_TOKEN does, so the pull request opens with no checks. A
+ # conflicted one does not care: CI fires when its author pushes a
+ # resolution. A clean one has nobody to push anything, so the comment
+ # posted on it asks for the one action that starts them.
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const {
MERGE_SHA, TARGET_BRANCH, PR_NUMBER, MANAGER,
BRANCH, VERSION, HAD_CONFLICT, CONFLICT_FILES, SUBJECT,
- FEATURE_ABSENT,
+ FEATURE_ABSENT, CLEAN,
} = process.env;
const { owner, repo } = context.repo;
const prNumber = Number(PR_NUMBER);
const hadConflict = HAD_CONFLICT === "true";
+ // Clean means the cherry-pick applied and the backported tree built
+ // green before the merge: nothing here needs a human's hands on the
+ // code, only the release manager's confirmation.
+ //
+ // CLEAN carries the pre-merge preflight's verdict, but the branch
+ // step above cherry-picked again just now, onto a release branch
+ // that may have moved since. Trust that fresher result too, or a
+ // race would open a tree full of conflict markers and call it
+ // conflict-free.
+ const clean = CLEAN === "true" && !hadConflict;
const runUrl =
`${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`;
@@ -904,11 +944,63 @@ jobs:
let pr = existing.data[0];
if (pr) {
core.info(`Backport PR already open: #${pr.number}; not duplicating.`);
+ // The branch was just force-pushed with a fresh cherry-pick onto
+ // the release branch as it stands now, which may have moved since
+ // the PR was opened. If that changed the outcome, the PR in front
+ // of the reviewer no longer describes its own branch -- a tree
+ // with conflict markers still reading "nothing here was edited by
+ // hand", or a resolved one still marked draft. Neither is safe to
+ // leave standing, and rewriting the PR from here would overwrite
+ // whatever a human has since done to it. Say so and stop.
+ if (pr.draft === clean) {
+ const wanted = clean ? "ready for review" : "a draft";
+ const note =
+ `This backport was re-run and its branch force-pushed. The ` +
+ `cherry-pick onto \`${TARGET_BRANCH}\` is now ` +
+ `**${clean ? "clean" : "conflicted"}**, so this PR should be ` +
+ `${wanted} — it is not, and what it says above no longer ` +
+ `describes its branch. Check the branch before reviewing: ` +
+ `[automation run](${runUrl}).`;
+ try {
+ await github.rest.issues.createComment({
+ owner, repo, issue_number: pr.number, body: note,
+ });
+ } catch (e) {
+ core.warning(`Could not comment on #${pr.number}: ${e.message}`);
+ }
+ try {
+ await github.rest.repos.createCommitStatus({
+ owner, repo, sha: MERGE_SHA, state: "failure",
+ context: `backport/${TARGET_BRANCH}`,
+ description: `Backport PR #${pr.number} no longer matches its branch`,
+ target_url: `${context.serverUrl}/${owner}/${repo}/pull/${pr.number}`,
+ });
+ } catch (e) {
+ core.warning(`Could not set commit status: ${e.message}`);
+ }
+ core.setFailed(
+ `#${pr.number} is ${pr.draft ? "a draft" : "ready for review"} but the ` +
+ `re-run cherry-pick is ${clean ? "clean" : "conflicted"}; it needs a look.`
+ );
+ return;
+ }
} else {
const conflictList = CONFLICT_FILES.trim()
? CONFLICT_FILES.trim().split(/\s+/).map((f) => `- \`${f}\``).join("\n")
: "";
- const statusLine = hadConflict
+ const statusLine = clean
+ ? `**Clean cherry-pick — nothing here was edited by hand.** It applied to ` +
+ `\`${TARGET_BRANCH}\` without conflicts, and the backported tree built ` +
+ `green before #${prNumber} merged.\n\n` +
+ `**Close this PR and reopen it to start its required checks.** This ` +
+ `pull request was opened by \`github-actions[bot]\`, and GitHub starts ` +
+ `no workflow run for anything the Actions token does, so it has no ` +
+ `checks yet. Reopening it (or pushing any commit to the branch) emits ` +
+ `the event they subscribe to. *Marking it ready for review will not* — ` +
+ `none of the three required workflows listen for that.\n\n` +
+ `After that: approve, and enable auto-merge if you would rather not ` +
+ `come back when the checks finish.`
+ : 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 ` +
@@ -979,9 +1071,13 @@ jobs:
``,
`### 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.`,
+ clean
+ ? `The backported tree built green before #${prNumber} merged. ` +
+ `Release-branch CI runs on this PR once it is reopened — see the ` +
+ `comment below.`
+ : `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?`,
``,
@@ -990,9 +1086,12 @@ jobs:
pr = (await github.rest.pulls.create({
owner, repo, base: TARGET_BRANCH, head: BRANCH,
- title, body, draft: true,
+ // A clean backport opens ready for review: nobody has code to
+ // write on it, and a draft would only suggest otherwise. It
+ // still needs its checks started -- the comment below says how.
+ title, body, draft: !clean,
})).data;
- core.info(`Opened draft backport PR #${pr.number}.`);
+ core.info(`Opened ${clean ? "ready" : "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).
@@ -1018,7 +1117,10 @@ jobs:
} catch (e) {
core.warning(`Could not fetch PR #${prNumber} author: ${e.message}`);
}
- if (author) {
+ // Only a backport somebody has to finish gets assigned. A clean
+ // one is the release manager's to review, and assigning its author
+ // would tell them they have work they do not have.
+ if (author && !clean) {
try {
await github.rest.issues.addAssignees({
owner, repo, issue_number: pr.number, assignees: [author],
@@ -1027,7 +1129,13 @@ jobs:
core.warning(`Could not assign ${author}: ${e.message}`);
}
}
- if (MANAGER && MANAGER !== author) {
+ // The backport PR is owned by github-actions[bot], so the source
+ // author is not its author and the guard below only exists to keep
+ // a conflicted backport's assignee from being asked to review their
+ // own resolution. A clean one has no assignee, and its manager must
+ // be asked even when they wrote the fix -- otherwise a fix authored
+ // by the release manager notifies nobody at all.
+ if (MANAGER && (clean || MANAGER !== author)) {
try {
await github.rest.pulls.requestReviewers({
owner, repo, pull_number: pr.number, reviewers: [MANAGER],
@@ -1045,11 +1153,17 @@ jobs:
try {
await github.rest.issues.createComment({
owner, repo, issue_number: prNumber,
- body:
- `Backport PR opened: draft #${pr.number} (${prUrl}) to ` +
- `\`${TARGET_BRANCH}\`` +
- (author ? `, assigned to @${author}` : "") +
- ` — needs manual work because ${reason}.`,
+ body: clean
+ ? `Backport PR opened: #${pr.number} (${prUrl}) to ` +
+ `\`${TARGET_BRANCH}\` — the cherry-pick was clean, so it is ` +
+ `ready for review` +
+ (MANAGER ? `, waiting on @${MANAGER}` : "") +
+ `. It needs to be reopened once to start its checks; the PR ` +
+ `says how.`
+ : `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}`);
@@ -1057,11 +1171,24 @@ jobs:
// A failure commit status on the merge commit keeps the "needs
// backport" signal visible on main next to the successful ones.
+ // A clean backport carries it too: it is open, not landed, and
+ // something still has to be done to it.
+ //
+ // Nothing updates this status afterwards -- not when the backport
+ // PR merges, not when it is closed. That predates this change and
+ // already leaves landed backports red (#8562's v1.2 status still
+ // names the draft #8585, which merged on 2026-09-19); routing the
+ // clean ones here widens it from some commits to all of them.
+ // Settling it needs a handler on the backport PR closing, which
+ // belongs with the removal of push-backports -- the only place in
+ // the repository that writes a success for this context.
try {
await github.rest.repos.createCommitStatus({
owner, repo, sha: MERGE_SHA, state: "failure",
context: `backport/${TARGET_BRANCH}`,
- description: `Draft backport PR #${pr.number} opened`,
+ description: clean
+ ? `Backport PR #${pr.number} open, awaiting review`
+ : `Draft backport PR #${pr.number} opened`,
target_url: prUrl,
});
} catch (e) {