[SPARK-58561][INFRA] Support backporting a PR merged into a non-default branch - #57764
[SPARK-58561][INFRA] Support backporting a PR merged into a non-default branch#57764uros-b wants to merge 4 commits into
Conversation
…lt branch in `merge_spark_pr.py` Detect an already-merged PR from `referenced` events when the `closed` event carries no commit, so a PR merged into a non-default branch (e.g. one opened against a rolling `branch-M.x`) can still be cherry-picked with the script. Default the backport prompt to the highest branch that has not already received the change, instead of the highest branch overall, which was the branch the PR had just merged into and produced an empty cherry-pick.
merge_spark_pr.py
cloud-fan
left a comment
There was a problem hiding this comment.
2 blocking, 0 non-blocking, 0 nits.
The intended backport flow has two correctness gaps: merge-commit identification can select an unrelated commit, and repeated invocations do not detect branches already backported.
Correctness (2)
- dev/merge_spark_pr.py:463: The referenced-event marker can match copied PR-body text and select the wrong commit for backporting. -- see inline
- dev/merge_spark_pr.py:1758: Repeated backport invocations default to a maintenance branch that already contains the change. -- see inline
Verification
I traced merge identification back to merge_pr: PR-body line starts are preserved in a git commit -m paragraph, so the fallback regex cannot identify the generated trailer by line anchoring alone. I also traced repeated backports: the closed-PR path performs one cherry_pick, exits, and on the next invocation rebuilds already_picked as only (target_ref,); it never checks which other release branches contain merge_hash.
PR description suggestions
- Correct the claim that line anchoring distinguishes the generated trailer from copied PR-body text.
- Document and handle repeated backport invocations: the current implementation does not discover maintenance branches that already contain the merge commit.
…eady-backported branches Address review feedback on the backport path: - Identify a merge commit by the footer structure `merge_pr` generates (a "Closes" paragraph followed immediately by the authors paragraph) rather than by the "Closes #N from " line alone. `merge_pr` passes the PR body through as its own commit-message paragraph, so a body quoting another PR's closing line kept that line at the start of a line and satisfied the old check. - Derive the already-picked set from the release branches that carry the merge footer, so a repeated invocation no longer defaults to a branch a previous run already backported to. `git branch --contains <merge_hash>` cannot see this: a cherry-pick is a new commit, and the footer is what `cherry-pick -x` copies, the same signal `dev/pr_merge_status.py` reads. - Loop in backport mode so one invocation can reach several maintenance branches, as the normal merge path already does.
uros-b
left a comment
There was a problem hiding this comment.
Thank you @cloud-fan! PTAL again
cloud-fan
left a comment
There was a problem hiding this comment.
1 addressed, 1 remaining, 2 new. (1 newly introduced, 1 late catch, 0 previously raised.)
3 blocking, 0 non-blocking, 0 nits.
The ordinary non-default-branch backport flow is improved, but merge-footer recognition and exhausted-branch selection still permit wrong or empty cherry-picks.
Remaining from prior review (1)
- The new matcher still scans the entire copied PR body, so a body that quotes a complete merge footer (for example while discussing or reverting a commit) satisfies the
ClosesplusAuthored-byregex. A referenced commit for a different PR can therefore still be selected as this PR's merge. Anchor the match to the final generated footer, or parse the final commit-message paragraphs, and add a regression case containing the complete quoted footer. -- existing thread
Correctness (2)
- dev/merge_spark_pr.py:727: This branch scan reintroduces the same false-positive class that
has_merge_footeris meant to avoid:git log --grepaccepts the fragment anywhere in the commit message. A body merely quotingCloses #N frommakes every containing release branch look already backported, so the script can skip the correct default and report a backport that never happened. Filter candidate commit messages with the validated footer matcher before mapping them to branches. -- see inline - dev/merge_spark_pr.py:771: When every known release branch is already in
already_picked, this fallback selectsbranch_names[0], which is known to contain the change. Backport mode callscherry_pickonce before asking whether another pick is wanted, so rerunning after all branches have received the PR immediately offers an empty cherry-pick. Stop with an 'all branches already contain this change' message instead of returning an already-picked default. -- see inline
Verification
I traced generated commit messages from merge_pr through both referenced-event selection and release-branch discovery, including PR bodies that contain footer-shaped paragraphs. I also traced backport mode when every known release branch is already present in picked_refs; no tests were run as part of this review.
PR description suggestions
- Correct the claim that a PR body cannot fake the complete footer structure; the body is copied verbatim and may quote an entire merge footer.
- Document and cover the case where every known release branch already contains the change.
…and stop when no branch remains Address the second round of review feedback: - Identify the generated footer by position rather than structure. A PR body is copied verbatim into the merge commit, so it can quote another commit's entire footer, authors paragraph included; matching the structure anywhere in the message therefore still selected the wrong commit. `merge_pr` appends the footer last, so `merge_footer_pr` reads the final "Closes" paragraph and compares its number. Cherry-pick provenance lines may follow it, but no later "Closes" paragraph can. - Validate the release-branch scan with the same matcher. `git log --grep` matches the fragment anywhere in a message, so a commit merely quoting the trailer made every containing branch look already backported; candidates are now confirmed before their branches count. - Return None from `default_pick_branch` when every known branch already has the change, and have both call sites report that instead of defaulting to a branch whose cherry-pick would be empty.
…nch message `ruff format` collapses the string and its `%` operand onto one line, which fits within the 100-character limit. Matches the sibling call site in the merge path.
What changes were proposed in this pull request?
Make
dev/merge_spark_pr.pyable to backport a PR that was merged into a non-default branch, and default its cherry-pick prompt to a branch that does not already have the change.Backport mode is entered when the script finds the commit that merged an already-closed PR. That lookup previously read the merge commit off the PR's
closedevent:GitHub only attributes a commit to the
closedevent when that commit lands on the default branch, because theCloses #Nkeyword in the commit message is what closes the PR and the keyword is honored only there. A PR merged into any other branch -- e.g. one opened against a rollingbranch-M.x-- is instead closed by this script through the API (close_pr), and thatclosedevent carries no commit. The merge then survives only as areferencedevent, somerge_commitscame up empty, backport mode never engaged, and the script fell through to the normal merge path -- offering to merge the PR a second time.This adds two helpers:
find_merge_commit(pr_num, pr_events)prefers theclosedevent's commit (unchanged behavior, and GitHub's own authoritative link), and only when that is absent falls back toreferencedevents. Since areferencedevent is raised by any commit merely mentioning the PR, each fallback candidate is confirmed against theCloses #N fromline thatmerge_prwrites into every merge commit.default_pick_branch(branch_names, already_picked)returns the highest-ranked release branch that has not already received the change. Backport mode previously defaulted the prompt tobranch_names[0], which for a PR merged intobranch-M.xis that very branch: accepting the default asked git to cherry-pick a commit onto the branch that already had it, which fails withThe previous cherry-pick is now emptyand lands the committer in the "Would you like to manually fix-up this merge?" prompt. The normal merge path already excludedtarget_reffrom its defaults, so this consolidates both call sites onto the shared helper.Why are the changes needed?
A PR opened against
branch-4.xand merged there cannot currently be backported tobranch-4.3with the merge script at all -- the script's own API-close erases the trail that its backport mode depends on. Concretely, for #57713 (merged intobranch-4.xas881e5a94a15), re-running the script prints:Answering
ythere would create a duplicate squash commit onbranch-4.x; the cherry-pick prompt is never reached. The only recourse was a manualgit cherry-pick -sxoutside the tool, which also skips the merge comment the script would post. With this change the same invocation reaches:Does this PR introduce any user-facing change?
No.
dev/merge_spark_pr.pyis a committer tool and is not part of any released artifact.How was this patch tested?
Added doctests for both new helpers; the script runs
doctest.testmod()on startup. The suite goes from 68 to 76 passing tests, 0 failures.Replayed the real GitHub event payloads for three PRs with
get_jsonstubbed to read genuine commit messages from a local clone (no network), asserting the resolved merge commit:branch-4.x881e5a94a15masterf0e2b19b82dmasterbf57ee78058Verified the fallback rejects false positives: [SPARK-58502][SQL] Make in-limit SQL test deterministic #57696's
referencedlist also contains881e5a94a15([SPARK-58502][SQL] Make in-limit SQL test deterministic #57713's commit, which mentions it). With [SPARK-58502][SQL] Make in-limit SQL test deterministic #57696'sclosedcommit stripped to force the fallback, the marker check still resolves tof0e2b19b82d.Verified a PR closed without merging yields
(None, None), so it does not wrongly enter backport mode.Verified the merge-path refactor is behavior-preserving by transcribing the previous
remaining_brancheslogic and diffing its prompt-default sequence againstdefault_pick_branchacross five multi-pick scenarios, including the Upstream-First two-branch path and a mid-listtarget_ref; all sequences match.Verified passing
already_picked=(target_ref,)in backport mode leaves the Upstream-First policy prompt unchanged by comparing_upstream_first_siblingunder the old and new arguments for every (target, pick) pair.Note:
ruffcould not be run in the authoring environment (no PyPI access); formatting follows the surrounding conventions in the file and is left to CI's lint job to confirm.Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 5)