From 4b7df2a1120f1f0e68a9d36a11725d7cc210b39c Mon Sep 17 00:00:00 2001 From: Yicong Huang <17627829+Yicong-Huang@users.noreply.github.com> Date: Fri, 1 May 2026 14:34:19 -0700 Subject: [PATCH] fix(ci): resolve merged PR robustly in Direct Backport Push MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The discover step relied solely on GET /commits/{sha}/pulls, whose backing index lags for tens of seconds after a squash merge. When the push event fires before the index catches up, the step finds no PR, push-backports is skipped, and the run becomes a no-op until someone re-runs it manually. Resolve the PR in two stages: 1. Parse the squash-merge commit message. .asf.yaml forces squash with PR_TITLE_AND_DESC, so the first line ends with "(#NNNN)". Extracting that number and fetching the PR directly via /pulls/{N} bypasses the association index entirely. 2. Fall back to /commits/{sha}/pulls with 5 attempts at exponential backoff (0, 2, 4, 8, 16 seconds — ~30s worst case) when the message does not match (manual or unconventional commits). Closes #4617 Co-Authored-By: Claude Opus 4.7 (1M context) --- .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 2bee5b53b69..37fbe04ce48 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", "");