This is an automated email from the ASF dual-hosted git repository.
uros-b pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/master by this push:
new d23432eed28d [SPARK-58734][INFRA] Skip a declined cherry-pick instead
of aborting the merge in merge_spark_pr.py
d23432eed28d is described below
commit d23432eed28d9637a04dff45c235cb16f2ece1e5
Author: Uros Bojanic <[email protected]>
AuthorDate: Thu Aug 13 08:45:51 2026 +0000
[SPARK-58734][INFRA] Skip a declined cherry-pick instead of aborting the
merge in merge_spark_pr.py
### What changes were proposed in this pull request?
This PR changes how `dev/merge_spark_pr.py` handles a committer declining
to resolve a conflicting backport cherry-pick.
Previously, when a cherry-pick onto a maintenance branch conflicted, the
script prompted `Would you like to manually fix-up this merge?`. Answering `N`
went through `continue_maybe(..., cherry=True)`, which aborted the cherry-pick
and then called `fail("Okay, exiting")` — terminating the whole run via
`sys.exit(-1)`. As a result the committer never reached the JIRA-resolution
step, and the process exited non-zero, even though the merge into the target
branch (and any earlier cherry-pi [...]
This PR makes declining a cherry-pick fix-up skip only that one branch and
continue:
- Add a `SkipCherryPick` exception. When a cherry-pick fix-up prompt is
declined, `continue_maybe` aborts the cherry-pick, restores the working tree
(`clean_up()`), and raises `SkipCherryPick` instead of calling `fail()`.
- `cherry_pick()` catches `SkipCherryPick` and returns only the picks that
actually landed (empty, or — in the Upstream-First two-branch path — just the
sibling branch that was already pushed).
- The two merge/backport loops in `main()` already consume the returned
list generically (`merged_refs + []` is a no-op), so no loop changes are
needed: after a skip they simply offer the next branch and still proceed to
resolve the associated JIRA.
Hard aborts elsewhere are unchanged (e.g. declining the push prompt, or
choosing `[a]bort` at the Upstream-First prompt, still exit).
### Why are the changes needed?
Backport cherry-pick conflicts are routine, and by the time one occurs the
merge into the target branch has already been pushed. Aborting the entire
script on a declined fix-up means the committer:
- skips JIRA resolution (the ticket is left Open, which is easy to miss and
tedious to reconcile after the fact), and
- gets a non-zero exit for what is a normal "do not backport that one
branch" decision.
Declining a single conflicting backport should skip just that branch and
let the merge finish cleanly.
### Does this PR introduce _any_ user-facing change?
No. This changes a committer-only developer tool (`dev/merge_spark_pr.py`);
it is not part of any Spark release artifact.
### How was this patch tested?
- The module's inline doctests still pass (run by `doctest.testmod()` at
startup): 76 attempted, 0 failed.
- `python3 -m py_compile dev/merge_spark_pr.py` is clean.
- Manually traced the affected control-flow paths:
- single-branch pick declined -> `cherry_pick` returns `[]`, the loop
re-prompts and JIRA resolution still runs;
- Upstream-First `[b]oth` path with the second pick declined -> the
already-pushed sibling branch is still returned and recorded in the merge
comment / `merged_refs`;
- declining non-cherry prompts, and `[a]bort` at the Upstream-First
prompt, still hard-exit as before.
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code with Claude Opus 4.8
Closes #57959 from uros-b/merge-script-cherrypick-skip.
Authored-by: Uros Bojanic <[email protected]>
Signed-off-by: Uros Bojanic <[email protected]>
---
dev/merge_spark_pr.py | 44 +++++++++++++++++++++++++++++++++++++-------
1 file changed, 37 insertions(+), 7 deletions(-)
diff --git a/dev/merge_spark_pr.py b/dev/merge_spark_pr.py
index 73258ff3816b..bb2b9733c976 100755
--- a/dev/merge_spark_pr.py
+++ b/dev/merge_spark_pr.py
@@ -556,6 +556,16 @@ def run_cmd(cmd):
return subprocess.check_output(cmd.split(" ")).decode("utf-8")
+class SkipCherryPick(Exception):
+ """Signals that the committer declined to resolve a conflicting
cherry-pick.
+
+ A backport conflict is routine, and by the time one is hit the merge into
the target
+ branch (and any earlier cherry-picks) has already been pushed. Declining
it must skip
+ only that one branch -- letting the caller offer another branch and,
crucially, still
+ resolve the JIRA -- instead of aborting the whole merge the way a hard
`fail()` would.
+ """
+
+
def continue_maybe(prompt, cherry=False):
if get_input(f"{prompt} (y/N): ", ["y", "n", ""]) != "y":
if cherry:
@@ -563,6 +573,9 @@ def continue_maybe(prompt, cherry=False):
run_cmd("git cherry-pick --abort")
except Exception:
print_error("Unable to abort and get back to the state before
cherry-pick")
+ print("Skipping this cherry-pick; the merge continues.")
+ clean_up()
+ raise SkipCherryPick()
fail("Okay, exiting")
@@ -654,7 +667,9 @@ def merge_pr(pr_num, target_ref, title, body, pr_repo_desc,
pr_author, co_author
def _do_cherry_pick(pr_num, merge_hash, pick_ref):
"""Cherry-pick `merge_hash` onto `pick_ref` and push.
- Returns the (pushed ref, pushed commit hash) pair.
+ Returns the (pushed ref, pushed commit hash) pair. Raises `SkipCherryPick`
if the
+ cherry-pick conflicts and the committer declines to resolve it, after
attempting to
+ abort the cherry-pick and restore the working tree.
"""
pick_branch_name = "%s_PICK_PR_%s_%s" % (BRANCH_PREFIX, pr_num,
pick_ref.upper())
@@ -764,7 +779,9 @@ def cherry_pick(pr_num, merge_hash, default_branch,
branch_names, target_ref, al
BOTH (the policy-compliant default) or branch-M.N only (treated as a
maintenance-only bugfix). Returns the list of (ref, commit_hash) pairs
actually
picked into, so the main loop can advance its remaining-branches list
correctly
- and record each backport commit for the merge comment.
+ and record each backport commit for the merge comment. The list is empty
(or, for the
+ Upstream-First two-branch path, holds only what landed) when the committer
declines to
+ resolve a conflict, so the caller simply offers the next branch and still
resolves JIRA.
"""
while True:
pick_ref = bold_input(f"Enter a branch name [{default_branch}]: ")
@@ -796,17 +813,30 @@ def cherry_pick(pr_num, merge_hash, default_branch,
branch_names, target_ref, al
{"b": ["b", "both", ""], "o": ["o", "only"], "a": ["a", "abort"]},
)
if choice == "b":
- picked_x = _do_cherry_pick(pr_num, merge_hash, sibling_x)
- picked_n = _do_cherry_pick(pr_num, merge_hash, pick_ref)
- return [picked_x, picked_n]
+ # Preserve any pick that was already pushed: if the branch-M.N
pick is skipped after
+ # branch-M.x landed, still return branch-M.x so it's recorded and
JIRA/comment
+ # reflect it.
+ picked = []
+ try:
+ picked.append(_do_cherry_pick(pr_num, merge_hash, sibling_x))
+ picked.append(_do_cherry_pick(pr_num, merge_hash, pick_ref))
+ except SkipCherryPick:
+ pass
+ return picked
elif choice == "o":
- return [_do_cherry_pick(pr_num, merge_hash, pick_ref)]
+ try:
+ return [_do_cherry_pick(pr_num, merge_hash, pick_ref)]
+ except SkipCherryPick:
+ return []
elif choice == "a":
fail("Aborted by user at Upstream-First policy prompt.")
else:
fail("Unrecognized choice %r; aborting." % choice)
- return [_do_cherry_pick(pr_num, merge_hash, pick_ref)]
+ try:
+ return [_do_cherry_pick(pr_num, merge_hash, pick_ref)]
+ except SkipCherryPick:
+ return []
# Common words carry no signal when comparing a PR title to a JIRA summary, so
they are
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]