This is an automated email from the ASF dual-hosted git repository.

Yicong-Huang pushed a commit to branch release/v1.1.0-incubating
in repository https://gitbox.apache.org/repos/asf/texera.git


The following commit(s) were added to refs/heads/release/v1.1.0-incubating by 
this push:
     new 1703832cc0 fix(ci): repair direct-backport-push YAML and post backport 
result comments (#4846)
1703832cc0 is described below

commit 1703832cc052ce8566115e1df3f7a381258e3c47
Author: Yicong Huang <[email protected]>
AuthorDate: Sun May 3 06:56:07 2026 +0000

    fix(ci): repair direct-backport-push YAML and post backport result comments 
(#4846)
    
    ### What changes were proposed in this PR?
    
    Three changes to `.github/workflows/direct-backport-push.yml`.
    
    **1. Repair YAML.** The inline `python3 -c '<source>'` from #4696 put
    Python at column 0 inside a `run: |` block indented at column 10. YAML
    treats `import re, sys` as a top-level key, so every push to `main`
    failed in 0 seconds with 0 jobs (e.g. [run
    25271247473](https://github.com/apache/texera/actions/runs/25271247473)).
    Python can't be re-indented (top-level statements reject leading
    whitespace), so the script moves to
    `.github/scripts/compose-backport-message.py`. Behavior unchanged.
    
    **2. Surface backport status on the original commit + PR.** Cherry-picks
    produce a new SHA, so the release branch never appears in the
    auto-derived branch badge on the main commit. Three channels instead —
    commit status badge, commit comment, PR comment — on success; commit
    status + PR comment on failure with an inline conflict diagnosis.
    
    Success PR comment:
    > Backport to [`release/0.4`](…/tree/release/0.4) succeeded as
    [`a1b2c3d`](…/commit/a1b2c3d…). [Run](…)
    
    Failure PR comment (when cherry-pick conflicts):
    > Backport to `release/0.4` failed. See [job log](…/job/…).
    >
    > **Conflicts in:**
    > - `f.txt`
    >
    > **Likely-missing prerequisites on main** (commits that touched these
    files between merge-base `6343a1bc` and `c027f3b2^` — consider
    backporting these first):
    > - `958b8e8 main: prereq edit f`
    
    Capped at 5 files / 10 commits; full detail stays in the job log.
    Rebase-race conflicts get the same shape but list the racing commits on
    `origin/<target>` instead.
    
    **3. Retry + structured logging.** `git push` retries 5x with `[0, 5,
    15, 30, 60]s` backoff and rebases on `origin/<target>` between attempts
    to absorb push races. Annotation API calls retry with `[0, 2, 5, 15]s`
    and degrade to warnings on final failure (a 5xx on a comment shouldn't
    undo a successful cherry-pick). Every phase is wrapped in `::group::`
    markers with a `[backport <target>] ...` prefix.
    
    ### Any related issues, documentation, discussions?
    
    Fixes the regression introduced in #4696.
    
    ### How was this PR tested?
    
    `yaml.safe_load` parses the workflow. `compose-backport-message.py`
    round-trips through `git interpret-trailers --parse` with
    `Co-authored-by` preserved. The conflict diagnosis output above came
    verbatim from a throwaway repo where main introduces a prerequisite edit
    + feature commit and the release branch touches the same lines.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 4.7, 1M context)
    
    ---------
    
    (backported from commit af5d174a8eb90896756b6c7610babec6fa4a9e98)
    
    Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
---
 .github/scripts/compose-backport-message.py |  56 ++++
 .github/workflows/direct-backport-push.yml  | 441 +++++++++++++++++++++++++---
 2 files changed, 452 insertions(+), 45 deletions(-)

diff --git a/.github/scripts/compose-backport-message.py 
b/.github/scripts/compose-backport-message.py
new file mode 100644
index 0000000000..90621be681
--- /dev/null
+++ b/.github/scripts/compose-backport-message.py
@@ -0,0 +1,56 @@
+# 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.
+
+# Composes the backport commit message: insert "(backported from commit X)"
+# between the message body and the trailer block (the trailing run of
+# `Key: value` lines such as Co-Authored-By and Signed-off-by) so trailers
+# stay contiguous at the bottom — that's where git itself parses them.
+#
+# The trailer block, by git convention, is the run of `Key: value` lines
+# after the LAST blank line in the message, and only counts if EVERY line
+# after that blank line is in trailer format. This avoids mis-detecting a
+# Conventional Commits subject like "feat: foo" or a body line like
+# "References:" as a trailer.
+#
+# Usage: original-message-on-stdin | compose-backport-message.py <merge-sha>
+
+import re
+import sys
+
+sha = sys.argv[1]
+msg = sys.stdin.read().rstrip("\n")
+lines = msg.split("\n")
+trailer_re = re.compile(r"^[A-Za-z][A-Za-z0-9-]*:\s")
+
+last_blank = -1
+for idx in range(len(lines) - 1, -1, -1):
+    if lines[idx] == "":
+        last_blank = idx
+        break
+
+trailer_start = len(lines)
+if last_blank != -1:
+    candidate = lines[last_blank + 1 :]
+    if candidate and all(trailer_re.match(line) for line in candidate):
+        trailer_start = last_blank + 1
+
+backport = f"(backported from commit {sha})"
+if trailer_start == len(lines):
+    print(msg + "\n\n" + backport)
+else:
+    body = "\n".join(lines[:trailer_start]).rstrip("\n")
+    trailers = "\n".join(lines[trailer_start:])
+    print(body + "\n\n" + backport + "\n\n" + trailers)
diff --git a/.github/workflows/direct-backport-push.yml 
b/.github/workflows/direct-backport-push.yml
index 215a888996..b9b92cdbfb 100644
--- a/.github/workflows/direct-backport-push.yml
+++ b/.github/workflows/direct-backport-push.yml
@@ -24,7 +24,9 @@ on:
 permissions:
   actions: read
   contents: write
-  pull-requests: read
+  issues: write
+  pull-requests: write
+  statuses: write
 
 jobs:
   discover:
@@ -195,17 +197,25 @@ jobs:
           # silences post-merge CI on backport commits.
           token: ${{ secrets.AUTO_MERGE_TOKEN || secrets.GITHUB_TOKEN }}
       - name: Cherry-pick merge commit onto target branch
+        id: cherry_pick
         env:
           MERGE_SHA: ${{ github.sha }}
           TARGET_BRANCH: ${{ matrix.target }}
         run: |
           set -euo pipefail
 
+          log() { printf '[backport %s] %s\n' "${TARGET_BRANCH}" "$*"; }
+          group() { printf '::group::%s\n' "$*"; }
+          endgroup() { printf '::endgroup::\n'; }
+
+          group "Validate merge commit ${MERGE_SHA}"
           parent_count=$(git rev-list --parents -n 1 "${MERGE_SHA}" | awk 
'{print NF-1}')
+          log "parent_count=${parent_count}"
           if [[ "${parent_count}" -ne 1 ]]; then
-            echo "Direct backport expects a squash-merged commit on main. 
${MERGE_SHA} has ${parent_count} parents." >&2
+            echo "::error::Direct backport expects a squash-merged commit on 
main. ${MERGE_SHA} has ${parent_count} parents."
             exit 1
           fi
+          endgroup
 
           git config user.name "github-actions[bot]"
           git config user.email "github-actions[bot]@users.noreply.github.com"
@@ -216,53 +226,394 @@ jobs:
           # (the original PR author for squash merges).
           original_author=$(git log -1 --format='%an <%ae>' "${MERGE_SHA}")
           merge_message=$(git log -1 --format=%B "${MERGE_SHA}")
+          log "original_author=${original_author}"
 
+          group "Cherry-pick onto ${TARGET_BRANCH}"
           git fetch --no-tags origin "${TARGET_BRANCH}"
           git checkout -B "${TARGET_BRANCH}" "origin/${TARGET_BRANCH}"
-          git cherry-pick --no-commit "${MERGE_SHA}"
-
-          # Compose the final commit message. The "(backported from commit X)"
-          # note goes between the message body and the trailer block (the
-          # trailing run of `Key: value` lines such as Co-Authored-By and
-          # Signed-off-by) so trailers stay contiguous at the bottom of the
-          # message — that's where git itself parses them.
-          #
-          # The trailer block, by git convention, is the run of `Key: value`
-          # lines after the LAST blank line in the message, and only counts
-          # if EVERY line after that blank line is in trailer format. This
-          # avoids mis-detecting a Conventional Commits subject like
-          # "feat: foo" or a body line like "References:" as a trailer.
+          base_sha=$(git rev-parse HEAD)
+          log "base_sha=${base_sha}"
+          if ! git cherry-pick --no-commit "${MERGE_SHA}"; then
+            endgroup
+            group "Conflict diagnosis"
+            conflicts=$(git diff --name-only --diff-filter=U)
+            log "Conflicted files:"
+            printf '  %s\n' ${conflicts}
+
+            # merge-base of the source commit and the target branch — the
+            # most recent point where main and the release branch shared
+            # history. Anything on main between the merge-base and the
+            # source commit that touches a conflicting file is a candidate
+            # "missing prerequisite" the backport probably needs first.
+            merge_base=$(git merge-base "${MERGE_SHA}^" 
"origin/${TARGET_BRANCH}" || echo "")
+            log ""
+            log "merge_base(${MERGE_SHA:0:8}^, 
origin/${TARGET_BRANCH})=${merge_base:-<none>}"
+
+            for f in ${conflicts}; do
+              log ""
+              log "── ${f} ──"
+              log "Conflict markers (line numbers in the working tree):"
+              grep -nE '^(<<<<<<<|=======|>>>>>>>)' -- "${f}" | head -40 || 
true
+
+              if [[ -n "${merge_base}" ]]; then
+                log ""
+                log "Commits on main that modified ${f} between 
${merge_base:0:8}..${MERGE_SHA:0:8}^ (likely-missing prerequisites — consider 
backporting these first):"
+                git log --oneline --no-merges "${merge_base}..${MERGE_SHA}^" 
-- "${f}" | head -20 || true
+
+                log ""
+                log "Commits on ${TARGET_BRANCH} that modified ${f} since 
${merge_base:0:8} (changes already on the release branch that diverged from 
main):"
+                git log --oneline --no-merges 
"${merge_base}..origin/${TARGET_BRANCH}" -- "${f}" | head -20 || true
+              fi
+
+              log ""
+              log "Last 3 commits anywhere that touched ${f}:"
+              git log --oneline --all -3 -- "${f}" || true
+            done
+            endgroup
+
+            # Write a condensed markdown summary for the failure PR comment.
+            # Caps: 5 files, 10 prerequisite commits total — keep PR
+            # comments scannable; the full detail is in the job log above.
+            diagnosis_file="${RUNNER_TEMP:-/tmp}/backport-diagnosis.md"
+            {
+              echo "**Conflicts in:**"
+              num=0
+              for f in ${conflicts}; do
+                if [[ ${num} -lt 5 ]]; then
+                  printf -- '- `%s`\n' "${f}"
+                fi
+                num=$((num + 1))
+              done
+              if [[ ${num} -gt 5 ]]; then
+                printf -- '- _(+%d more)_\n' "$((num - 5))"
+              fi
+              if [[ -n "${merge_base}" ]]; then
+                echo
+                echo "**Likely-missing prerequisites on main** (commits that 
touched these files between merge-base \`${merge_base:0:8}\` and 
\`${MERGE_SHA:0:8}^\` — consider backporting these first):"
+                {
+                  for f in ${conflicts}; do
+                    git log --oneline --no-merges 
"${merge_base}..${MERGE_SHA}^" -- "${f}" || true
+                  done
+                } | sort -u | head -10 | while IFS= read -r line; do
+                  [[ -n "${line}" ]] && printf -- '- `%s`\n' "${line}"
+                done
+              fi
+            } > "${diagnosis_file}"
+            log "Wrote diagnosis summary to ${diagnosis_file}"
+
+            echo "::error::Cherry-pick of ${MERGE_SHA} onto ${TARGET_BRANCH} 
hit conflicts. See 'Conflict diagnosis' group above for likely-missing 
prerequisite commits and per-file conflict markers."
+            exit 1
+          fi
+          endgroup
+
+          group "Compose backport commit message"
           new_message=$(
-            printf '%s' "${merge_message}" | \
-              python3 -c '
-import re, sys
-sha = sys.argv[1]
-msg = sys.stdin.read().rstrip("\n")
-lines = msg.split("\n")
-trailer_re = re.compile(r"^[A-Za-z][A-Za-z0-9-]*:\s")
-
-last_blank = -1
-for idx in range(len(lines) - 1, -1, -1):
-    if lines[idx] == "":
-        last_blank = idx
-        break
-
-trailer_start = len(lines)
-if last_blank != -1:
-    candidate = lines[last_blank + 1:]
-    if candidate and all(trailer_re.match(l) for l in candidate):
-        trailer_start = last_blank + 1
-
-backport = f"(backported from commit {sha})"
-if trailer_start == len(lines):
-    print(msg + "\n\n" + backport)
-else:
-    body = "\n".join(lines[:trailer_start]).rstrip("\n")
-    trailers = "\n".join(lines[trailer_start:])
-    print(body + "\n\n" + backport + "\n\n" + trailers)
-' "${MERGE_SHA}"
+            printf '%s' "${merge_message}" \
+              | python3 .github/scripts/compose-backport-message.py 
"${MERGE_SHA}"
           )
-
           printf '%s\n' "${new_message}" | git commit -F - 
--author="${original_author}"
+          log "local_sha=$(git rev-parse HEAD)"
+          endgroup
 
-          git push origin "HEAD:${TARGET_BRANCH}"
+          # Push with retry. Transient failures (network, GitHub 5xx) are pure
+          # backoff. Non-fast-forward (race with another push to the same
+          # release branch) refreshes origin/<target> and rebases this single
+          # cherry-pick on top before retrying.
+          push_attempts=5
+          push_backoffs=(0 5 15 30 60)
+          push_success=0
+          for i in $(seq 0 $((push_attempts - 1))); do
+            if [[ "${push_backoffs[i]}" -gt 0 ]]; then
+              log "Push attempt $((i + 1))/${push_attempts}: sleeping 
${push_backoffs[i]}s"
+              sleep "${push_backoffs[i]}"
+            fi
+            group "Push attempt $((i + 1))/${push_attempts}"
+            if git push origin "HEAD:${TARGET_BRANCH}" 2>&1; then
+              push_success=1
+              endgroup
+              break
+            fi
+            push_rc=$?
+            log "git push exit code=${push_rc}"
+            endgroup
+
+            # Refresh origin and rebase before retrying. If the remote did not
+            # advance, this is a no-op rebase and the next push will likely
+            # hit the same transient error — backoff handles that.
+            log "Refreshing origin/${TARGET_BRANCH} before retry"
+            git fetch --no-tags origin "${TARGET_BRANCH}"
+            old_remote_head="${remote_head:-${base_sha}}"
+            remote_head=$(git rev-parse "origin/${TARGET_BRANCH}")
+            log "origin/${TARGET_BRANCH}=${remote_head}"
+            if ! git rebase "origin/${TARGET_BRANCH}"; then
+              conflicts=$(git diff --name-only --diff-filter=U)
+              group "Rebase conflict diagnosis"
+              log "Conflicted files during rebase:"
+              printf '  %s\n' ${conflicts}
+              log ""
+              log "Commits on ${TARGET_BRANCH} that landed since this run 
started (${old_remote_head:0:8}..${remote_head:0:8}):"
+              git log --oneline --no-merges 
"${old_remote_head}..${remote_head}" | head -20 || true
+              for f in ${conflicts}; do
+                log ""
+                log "── ${f} ──"
+                log "Commits in ${old_remote_head:0:8}..${remote_head:0:8} 
that touched ${f}:"
+                git log --oneline --no-merges 
"${old_remote_head}..${remote_head}" -- "${f}" | head -20 || true
+              done
+              endgroup
+
+              diagnosis_file="${RUNNER_TEMP:-/tmp}/backport-diagnosis.md"
+              {
+                printf -- '**Rebase conflict during push** — another commit 
landed on `%s` between the start of this run and the push attempt.\n\n' 
"${TARGET_BRANCH}"
+                echo "**Conflicts in:**"
+                num=0
+                for f in ${conflicts}; do
+                  if [[ ${num} -lt 5 ]]; then
+                    printf -- '- `%s`\n' "${f}"
+                  fi
+                  num=$((num + 1))
+                done
+                if [[ ${num} -gt 5 ]]; then
+                  printf -- '- _(+%d more)_\n' "$((num - 5))"
+                fi
+                echo
+                printf -- '**Racing commits on `%s`** (`%s..%s`):\n' \
+                  "${TARGET_BRANCH}" "${old_remote_head:0:8}" 
"${remote_head:0:8}"
+                git log --oneline --no-merges 
"${old_remote_head}..${remote_head}" | head -10 | while IFS= read -r line; do
+                  [[ -n "${line}" ]] && printf -- '- `%s`\n' "${line}"
+                done
+              } > "${diagnosis_file}"
+              log "Wrote diagnosis summary to ${diagnosis_file}"
+
+              git rebase --abort || true
+              echo "::error::Rebase onto refreshed origin/${TARGET_BRANCH} hit 
a conflict; another commit changed the same lines. See 'Rebase conflict 
diagnosis' group for the racing commits."
+              exit 1
+            fi
+          done
+
+          if [[ "${push_success}" -ne 1 ]]; then
+            echo "::error::git push to ${TARGET_BRANCH} failed after 
${push_attempts} attempts"
+            exit 1
+          fi
+
+          new_sha=$(git rev-parse HEAD)
+          log "new_sha=${new_sha}"
+          echo "new_sha=${new_sha}" >> "$GITHUB_OUTPUT"
+
+      - name: Annotate original PR and commit on success
+        if: success()
+        uses: actions/github-script@v8
+        env:
+          MERGE_SHA: ${{ github.sha }}
+          TARGET_BRANCH: ${{ matrix.target }}
+          NEW_SHA: ${{ steps.cherry_pick.outputs.new_sha }}
+          PR_NUMBER: ${{ needs.discover.outputs.pr_number }}
+        with:
+          script: |
+            const { MERGE_SHA, TARGET_BRANCH, NEW_SHA, PR_NUMBER } = 
process.env;
+            const { owner, repo } = context.repo;
+            const runUrl =
+              
`${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`;
+            const newCommitUrl =
+              `${context.serverUrl}/${owner}/${repo}/commit/${NEW_SHA}`;
+            const branchUrl =
+              `${context.serverUrl}/${owner}/${repo}/tree/${TARGET_BRANCH}`;
+
+            core.info(
+              `Annotating: merge_sha=${MERGE_SHA} new_sha=${NEW_SHA} ` +
+              `target=${TARGET_BRANCH} pr=${PR_NUMBER || '<unknown>'}`,
+            );
+
+            // Annotation API calls are best-effort: a transient 5xx shouldn't
+            // demote the whole job to failure when the cherry-pick itself
+            // succeeded. Each call gets a small bounded retry; if it still
+            // fails we log a warning and move on so the other annotations
+            // still get a chance to land.
+            const backoffsMs = [0, 2000, 5000, 15000];
+            async function withRetry(name, fn) {
+              for (let i = 0; i < backoffsMs.length; i++) {
+                if (backoffsMs[i] > 0) {
+                  core.info(
+                    `Retrying ${name} in ${backoffsMs[i] / 1000}s ` +
+                    `(attempt ${i + 1}/${backoffsMs.length}).`,
+                  );
+                  await new Promise((r) => setTimeout(r, backoffsMs[i]));
+                }
+                try {
+                  const out = await fn();
+                  core.info(`${name} ok.`);
+                  return out;
+                } catch (e) {
+                  const msg = `${name} failed (status ${e.status ?? '?'}): 
${e.message}`;
+                  if (i === backoffsMs.length - 1) {
+                    core.warning(`${msg} — giving up.`);
+                    return null;
+                  }
+                  core.warning(`${msg} — will retry.`);
+                }
+              }
+            }
+
+            // GitHub auto-derives the branch badges shown next to a commit
+            // title from "branches that contain this commit". A cherry-pick
+            // produces a different SHA than the main commit, so the release
+            // branch will never naturally appear on the main commit's page.
+            // Two surfacing channels instead:
+            //   1. A commit status — appears as a green check badge in the
+            //      same row as CI statuses on the commit and any PRs that
+            //      reference it. target_url drops the user on the new commit.
+            //   2. A commit comment with the same info, for richer detail.
+            await withRetry("createCommitStatus", () =>
+              github.rest.repos.createCommitStatus({
+                owner,
+                repo,
+                sha: MERGE_SHA,
+                state: "success",
+                context: `backport/${TARGET_BRANCH}`,
+                description: `Backported as ${NEW_SHA.slice(0, 7)}`,
+                target_url: newCommitUrl,
+              }),
+            );
+
+            await withRetry("createCommitComment", () =>
+              github.rest.repos.createCommitComment({
+                owner,
+                repo,
+                commit_sha: MERGE_SHA,
+                body:
+                  `Backported to [\`${TARGET_BRANCH}\`](${branchUrl}) as ` +
+                  `[\`${NEW_SHA.slice(0, 7)}\`](${newCommitUrl}). ` +
+                  `[Run](${runUrl})`,
+              }),
+            );
+
+            if (PR_NUMBER) {
+              await withRetry("createPRComment", () =>
+                github.rest.issues.createComment({
+                  owner,
+                  repo,
+                  issue_number: Number(PR_NUMBER),
+                  body:
+                    `Backport to [\`${TARGET_BRANCH}\`](${branchUrl}) 
succeeded ` +
+                    `as [\`${NEW_SHA.slice(0, 7)}\`](${newCommitUrl}). ` +
+                    `[Run](${runUrl})`,
+                }),
+              );
+            } else {
+              core.info("No PR number resolved — skipping PR comment.");
+            }
+
+      - name: Annotate original PR and commit on failure
+        if: failure()
+        uses: actions/github-script@v8
+        env:
+          MERGE_SHA: ${{ github.sha }}
+          TARGET_BRANCH: ${{ matrix.target }}
+          PR_NUMBER: ${{ needs.discover.outputs.pr_number }}
+        with:
+          script: |
+            const { MERGE_SHA, TARGET_BRANCH, PR_NUMBER } = process.env;
+            const { owner, repo } = context.repo;
+            const runUrl =
+              
`${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`;
+
+            core.info(
+              `Annotating failure: merge_sha=${MERGE_SHA} ` +
+              `target=${TARGET_BRANCH} pr=${PR_NUMBER || '<unknown>'}`,
+            );
+
+            const backoffsMs = [0, 2000, 5000, 15000];
+            async function withRetry(name, fn) {
+              for (let i = 0; i < backoffsMs.length; i++) {
+                if (backoffsMs[i] > 0) {
+                  core.info(
+                    `Retrying ${name} in ${backoffsMs[i] / 1000}s ` +
+                    `(attempt ${i + 1}/${backoffsMs.length}).`,
+                  );
+                  await new Promise((r) => setTimeout(r, backoffsMs[i]));
+                }
+                try {
+                  const out = await fn();
+                  core.info(`${name} ok.`);
+                  return out;
+                } catch (e) {
+                  const msg = `${name} failed (status ${e.status ?? '?'}): 
${e.message}`;
+                  if (i === backoffsMs.length - 1) {
+                    core.warning(`${msg} — giving up.`);
+                    return null;
+                  }
+                  core.warning(`${msg} — will retry.`);
+                }
+              }
+            }
+
+            // Find this matrix leg's job so the link drops the user directly
+            // onto the failing log instead of the run summary.
+            let jobUrl = runUrl;
+            const jobs = await withRetry("listJobsForWorkflowRun", () =>
+              github.paginate(github.rest.actions.listJobsForWorkflowRun, {
+                owner,
+                repo,
+                run_id: context.runId,
+                per_page: 100,
+              }),
+            );
+            if (jobs) {
+              const me = jobs.find((j) => 
j.name.includes(`(${TARGET_BRANCH})`));
+              if (me?.html_url) {
+                jobUrl = me.html_url;
+                core.info(`Resolved job URL for matrix leg: ${jobUrl}`);
+              } else {
+                core.info(
+                  `No job matched name including "(${TARGET_BRANCH})"; ` +
+                  "falling back to run URL.",
+                );
+              }
+            }
+
+            await withRetry("createCommitStatus", () =>
+              github.rest.repos.createCommitStatus({
+                owner,
+                repo,
+                sha: MERGE_SHA,
+                state: "failure",
+                context: `backport/${TARGET_BRANCH}`,
+                description: "Backport failed",
+                target_url: jobUrl,
+              }),
+            );
+
+            // Pick up the markdown diagnosis the bash step wrote on
+            // conflict (cherry-pick or rebase). Missing file just means
+            // the failure happened elsewhere (e.g. push 5xx after retries,
+            // permissions) — we still post the basic comment.
+            const fs = require("fs");
+            const diagPath =
+              `${process.env.RUNNER_TEMP || "/tmp"}/backport-diagnosis.md`;
+            let diagnosis = "";
+            try {
+              diagnosis = fs.readFileSync(diagPath, "utf8").trim();
+              if (diagnosis) {
+                core.info(`Found diagnosis at ${diagPath} (${diagnosis.length} 
chars)`);
+              }
+            } catch (e) {
+              core.info(
+                `No diagnosis file at ${diagPath} (${e.code}) — failure likely 
not a conflict.`,
+              );
+            }
+
+            if (PR_NUMBER) {
+              const head =
+                `Backport to \`${TARGET_BRANCH}\` failed. ` +
+                `See [job log](${jobUrl}).`;
+              const body = diagnosis ? `${head}\n\n${diagnosis}` : head;
+              await withRetry("createPRComment", () =>
+                github.rest.issues.createComment({
+                  owner,
+                  repo,
+                  issue_number: Number(PR_NUMBER),
+                  body,
+                }),
+              );
+            } else {
+              core.info("No PR number resolved — skipping PR comment.");
+            }

Reply via email to