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 <[email protected]>
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) {

Reply via email to