This is an automated email from the ASF dual-hosted git repository.
Yicong-Huang 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 ddb5c401a1 fix(ci): resolve merged PR robustly in Direct Backport Push
(#4620)
ddb5c401a1 is described below
commit ddb5c401a18abb76b0c36adaf4cbbbc8db051549
Author: Yicong Huang <[email protected]>
AuthorDate: Fri May 1 14:52:01 2026 -0700
fix(ci): resolve merged PR robustly in Direct Backport Push (#4620)
### What changes were proposed in this PR?
The `Discover direct backport targets` step in
`direct-backport-push.yml` previously relied solely on `GET
/commits/{sha}/pulls`. That endpoint is backed by an async association
index that lags for tens of seconds after a squash merge — see [run
25233602734 attempt
1](https://github.com/apache/texera/actions/runs/25233602734) for #4600,
which logged `No merged pull request is associated with ...` and skipped
`push-backports` even though the PR carried a `release/*` label.
Resolve the PR in two stages, in order:
1. **Parse the squash-merge commit message.** `.asf.yaml` forces squash
merges with `PR_TITLE_AND_DESC`, so the first line of every merge commit
on `main` ends with `(#NNNN)`. Regex-extracting that number and fetching
the PR directly via `/pulls/{N}` bypasses the association index
entirely. We also verify `pr.merged === true` before trusting the
result.
2. **Fall back to `/commits/{sha}/pulls` with retries.** If the message
does not match (manual or unconventional commits), retry the original
API call up to 5 times with exponential backoff (`0s, 2s, 4s, 8s, 16s` —
~30s worst case).
### Any related issues, documentation, discussions?
Closes #4617
### How was this PR tested?
YAML parses locally (`python3 -c "import yaml; yaml.safe_load(...)"`).
The fix runs the same final code path as before — only the resolution
method changes — so existing `push-backports` behavior is unchanged once
a PR is found. Will be exercised the next time a PR with `release/*`
labels merges to `main`.
### Was this PR authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 4.7)
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
---
.github/workflows/direct-backport-push.yml | 65 ++++++++++++++++++++++++++----
1 file changed, 57 insertions(+), 8 deletions(-)
diff --git a/.github/workflows/direct-backport-push.yml
b/.github/workflows/direct-backport-push.yml
index 2bee5b53b6..37fbe04ce4 100644
--- a/.github/workflows/direct-backport-push.yml
+++ b/.github/workflows/direct-backport-push.yml
@@ -43,16 +43,65 @@ jobs:
const sha = context.sha;
const { owner, repo } = context.repo;
- const response = await github.request(
- "GET /repos/{owner}/{repo}/commits/{commit_sha}/pulls",
- {
- owner,
- repo,
- commit_sha: sha,
+ // Strategy 1 (preferred): parse the squash-merge commit message.
+ // ASF .asf.yaml forces squash merges with PR_TITLE_AND_DESC, so
the
+ // first line ends with "(#NNNN)". This is deterministic and avoids
+ // the commit↔PR association index, which can lag for tens of
seconds
+ // after a merge.
+ async function resolvePrFromMessage() {
+ const message = context.payload?.head_commit?.message ?? "";
+ const firstLine = message.split("\n", 1)[0];
+ const match = firstLine.match(/\(#(\d+)\)\s*$/);
+ if (!match) {
+ core.info('Commit message does not end with "(#N)"; falling
back to API.');
+ return null;
}
- );
+ const prNumber = Number(match[1]);
+ try {
+ const { data: pr } = await github.rest.pulls.get({
+ owner,
+ repo,
+ pull_number: prNumber,
+ });
+ if (!pr.merged) {
+ core.warning(`PR #${prNumber} extracted from commit message
is not merged; falling back to API.`);
+ return null;
+ }
+ core.info(`Resolved PR #${prNumber} from commit message.`);
+ return pr;
+ } catch (e) {
+ core.warning(`Failed to fetch PR #${prNumber}: ${e.message}.
Falling back to API.`);
+ return null;
+ }
+ }
+
+ // Strategy 2 (fallback): GET /commits/{sha}/pulls with exponential
+ // backoff. 5 attempts at 0/2/4/8/16s — total worst case ~30s.
+ async function resolvePrFromApi() {
+ const backoffsMs = [0, 2000, 4000, 8000, 16000];
+ for (let i = 0; i < backoffsMs.length; i++) {
+ if (backoffsMs[i] > 0) {
+ core.info(`Retrying commit→PR lookup in ${backoffsMs[i] /
1000}s (attempt ${i + 1}/${backoffsMs.length}).`);
+ await new Promise((resolve) => setTimeout(resolve,
backoffsMs[i]));
+ }
+ const response = await github.request(
+ "GET /repos/{owner}/{repo}/commits/{commit_sha}/pulls",
+ {
+ owner,
+ repo,
+ commit_sha: sha,
+ }
+ );
+ const pr = response.data.find((p) => p.merge_commit_sha ===
sha) ?? response.data[0];
+ if (pr) {
+ core.info(`Resolved PR #${pr.number} from
commits/${sha}/pulls on attempt ${i + 1}.`);
+ return pr;
+ }
+ }
+ return null;
+ }
- const pullRequest = response.data.find((pr) => pr.merge_commit_sha
=== sha) ?? response.data[0];
+ const pullRequest = (await resolvePrFromMessage()) ?? (await
resolvePrFromApi());
if (!pullRequest) {
core.info(`No merged pull request is associated with ${sha}.`);
core.setOutput("pr_number", "");