This is an automated email from the ASF dual-hosted git repository.
zhengruifeng 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 53c26da1cbfa [SPARK-58905][INFRA] Allow merge script to update
resolved JIRA fix versions
53c26da1cbfa is described below
commit 53c26da1cbfaf7133bb596146323c44bd49a0c0a
Author: Ruifeng Zheng <[email protected]>
AuthorDate: Tue Aug 25 08:47:31 2026 +0800
[SPARK-58905][INFRA] Allow merge script to update resolved JIRA fix versions
### What changes were proposed in this pull request?
This PR allows `dev/merge_spark_pr.py` to update Fix Versions when the
linked JIRA
issue is already Resolved or Closed during the normal merge cleanup path.
The script already supports adding additional Fix Versions to a resolved
issue; this
change allows the regular post-merge JIRA update path to reach that logic.
It also
updates Fix Version inference so a PR targeting a release integration
branch such as
`branch-4.x` can infer that line's latest unreleased Fix Version even when
`master`
is not part of the same merge invocation.
### Why are the changes needed?
When merging a PR whose JIRA issue was already resolved, the merge script
printed that
the issue already had status `Resolved` and returned before prompting for
Fix Versions.
This prevented committers from correcting or adding the Fix Version during
merge.
For example, this happened while merging #58150: the linked issue
`SPARK-58880` was
already `Resolved`, so the script printed `JIRA issue SPARK-58880 already
has status
'Resolved'` and did not give a chance to update the JIRA Fix Versions.
This also matters for separate release-branch PRs. If a change is merged to
`master`
first and later backported through a separate PR targeting `branch-4.x`,
the linked
JIRA is already resolved from the `master` merge, but the release-branch
merge still
needs to add the `branch-4.x` Fix Version.
### Does this PR introduce _any_ user-facing change?
No.
### How was this patch tested?
Manually verified with:
```
git diff --check
conda run -n spark-dev-313 python -m py_compile dev/merge_spark_pr.py
cd dev
conda run -n spark-dev-313 python -m doctest -v merge_spark_pr.py
```
The doctest run passed with 80 tests.
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: OpenAI Codex GPT-5
Closes #58165 from zhengruifeng/fix-merge-script-resolved-jira.
Authored-by: Ruifeng Zheng <[email protected]>
Signed-off-by: Ruifeng Zheng <[email protected]>
---
dev/merge_spark_pr.py | 64 ++++++++++++++++++++++++++++++++-------------------
1 file changed, 40 insertions(+), 24 deletions(-)
diff --git a/dev/merge_spark_pr.py b/dev/merge_spark_pr.py
index 291f09d2635e..7a06d1c5dd33 100755
--- a/dev/merge_spark_pr.py
+++ b/dev/merge_spark_pr.py
@@ -180,7 +180,7 @@ def compute_merge_default_fix_versions(merge_branches,
unreleased_version_names)
contain the commit, leveraging the Upstream-First backporting policy
(cherry-picks flow
master -> branch-M.x -> branch-M.N):
- master contributes the greatest unreleased N.0.0;
- - branch-M.x with master contributes that major's greatest unreleased
minor.0;
+ - branch-M.x contributes that major's greatest unreleased minor.0;
- branch-M.N contributes its greatest unreleased M.N.patch.
Redundant entries are then suppressed: master's N.0.0 is dropped when any
branch-M.x is in
the merge set (a cherry-pick to branch-M.x has already landed on master);
branch-M.x's
@@ -217,7 +217,7 @@ def compute_merge_default_fix_versions(merge_branches,
unreleased_version_names)
([], 1, True)
>>> compute_merge_default_fix_versions(["branch-4.x"], ["4.3.0"])
- ([], [])
+ (['4.3.0'], [])
>>> d, w = compute_merge_default_fix_versions(["branch-4.99"], ["4.3.0"])
>>> d == [] and len(w) == 1 and "branch-4.99" in w[0]
@@ -277,8 +277,6 @@ def compute_merge_default_fix_versions(merge_branches,
unreleased_version_names)
continue
line_major = _integration_major_from_branch(b)
if line_major is not None:
- if "master" not in merge_branches:
- continue
line_versions = [n for n in names if re.match(r"^%s\.\d+\.\d+$" %
line_major, n)]
chosen = _semver_max_version(line_versions)
if chosen:
@@ -286,7 +284,7 @@ def compute_merge_default_fix_versions(merge_branches,
unreleased_version_names)
else:
warnings.append(
"Could not infer an unreleased Spark %s
(minor.maintenance) fix version "
- "for branch-%s.x + master merge; enter version(s) manually
when prompted."
+ "for branch-%s.x merge; enter version(s) manually when
prompted."
% (line_major, line_major)
)
continue
@@ -920,7 +918,15 @@ def title_similarity(pr_title, summary):
def format_jira_verification(
- pr_num, pr_title, jira_id, summary, status, issuetype, use_color=False,
is_followup=False
+ pr_num,
+ pr_title,
+ jira_id,
+ summary,
+ status,
+ issuetype,
+ resolution=None,
+ use_color=False,
+ is_followup=False,
):
"""Render the JIRA-vs-PR match block shown before merging.
@@ -987,6 +993,7 @@ def format_jira_verification(
Match: 0.00 (FOLLOWUP: title intentionally differs, not checked)
"""
status_warning = ""
+ resolution_suffix = " (%s)" % resolution if resolution else ""
if status in ("Resolved", "Closed"):
status_warning = " <-- WARNING: already Resolved/Closed"
if use_color:
@@ -1007,7 +1014,7 @@ def format_jira_verification(
"=== Verify JIRA matches PR #%s ===" % pr_num,
"PR title: %s" % pr_title,
"JIRA %s: %s" % (jira_id, summary),
- " Status: %s%s" % (status, status_warning),
+ " Status: %s%s%s" % (status, resolution_suffix, status_warning),
" Type: %s" % issuetype,
" Match: %.2f%s" % (score, match_suffix),
]
@@ -1021,6 +1028,9 @@ def print_jira_issue_summary(issue):
assignee = assignee.displayName
assignee = "Assignee\t%s\n" % assignee
status = "Status\t\t%s\n" % issue.fields.status.name
+ resolution = ""
+ if issue.fields.resolution is not None:
+ resolution = "Resolution\t%s\n" % issue.fields.resolution.name
components = "Components\t%s\n" % [x.name for x in issue.fields.components]
url = "Url\t\t%s/%s\n" % (JIRA_BASE, issue.key)
target_versions = "Affected\t%s\n" % [x.name for x in
issue.fields.versions]
@@ -1029,8 +1039,8 @@ def print_jira_issue_summary(issue):
fix_versions = "Fixed\t\t%s\n" % [x.name for x in
issue.fields.fixVersions]
print("=== JIRA %s ===" % issue.key)
print(
- "%s%s%s%s%s%s%s"
- % (summary, assignee, status, components, url, target_versions,
fix_versions)
+ "%s%s%s%s%s%s%s%s"
+ % (summary, assignee, status, resolution, components, url,
target_versions, fix_versions)
)
@@ -1110,7 +1120,7 @@ def reconcile_jira_components(issue, title_components):
print_error("Failed to update components on JIRA %s: %s" % (issue.key,
e))
-def get_jira_issue(prompt, default_jira_id="", allow_resolved=False):
+def get_jira_issue(prompt, default_jira_id=""):
jira_id = bold_input("%s [%s]: " % (prompt, default_jira_id))
if jira_id == "":
jira_id = default_jira_id
@@ -1122,21 +1132,24 @@ def get_jira_issue(prompt, default_jira_id="",
allow_resolved=False):
print_jira_issue_summary(issue)
status = issue.fields.status.name
if status == "Resolved" or status == "Closed":
- print("JIRA issue %s already has status '%s'" % (jira_id, status))
- if not allow_resolved:
+ resolution = issue.fields.resolution
+ resolution_name = resolution.name if resolution is not None else
None
+ print("JIRA issue %s already has status '%s' (%s)" % (jira_id,
status, resolution_name))
+ # Only a ticket an earlier merge resolved as Fixed can
legitimately gain
+ # another Fix Version. Duplicate / Won't Fix / Invalid tickets
must not be
+ # touched.
+ if resolution_name != "Fixed":
return None
if get_input("Check if the JIRA information is as expected (y/N): ",
["y", "n", ""]) == "y":
return issue
else:
return get_jira_issue(
"Enter the revised JIRA ID again or leave blank to skip",
- allow_resolved=allow_resolved,
)
except Exception as e:
print_error("ASF JIRA could not find %s: %s" % (jira_id, e))
return get_jira_issue(
"Enter the revised JIRA ID again or leave blank to skip",
- allow_resolved=allow_resolved,
)
@@ -1145,9 +1158,8 @@ def resolve_jira_issue(
comment,
default_jira_id="",
title_components=(),
- allow_resolved=False,
):
- issue = get_jira_issue("Enter a JIRA id", default_jira_id, allow_resolved)
+ issue = get_jira_issue("Enter a JIRA id", default_jira_id)
if issue is None:
return
@@ -1188,6 +1200,12 @@ def resolve_jira_issue(
% issue.key
)
return
+ print(
+ "JIRA issue %s has fix version(s) %s; inferred addition(s): %s"
+ % (issue.key, existing_fix_version_names, default_fix_list)
+ )
+ if get_input("Add these fix version(s)? (y/N): ", ["y", "n", ""]) !=
"y":
+ return
default_fix_versions = ",".join(default_fix_list)
available_versions = set(list(map(lambda v: v.name, versions)))
@@ -1331,7 +1349,7 @@ def assign_issue(issue: int, assignee: str) -> bool:
return True
-def resolve_jira_issues(title, merge_branches, comment, title_components=(),
allow_resolved=False):
+def resolve_jira_issues(title, merge_branches, comment, title_components=()):
jira_ids = re.findall("SPARK-[0-9]{4,5}", title)
if len(jira_ids) == 0:
@@ -1339,7 +1357,6 @@ def resolve_jira_issues(title, merge_branches, comment,
title_components=(), all
merge_branches,
comment,
title_components=title_components,
- allow_resolved=allow_resolved,
)
for jira_id in jira_ids:
resolve_jira_issue(
@@ -1347,11 +1364,10 @@ def resolve_jira_issues(title, merge_branches, comment,
title_components=(), all
comment,
jira_id,
title_components=title_components,
- allow_resolved=allow_resolved,
)
-def update_jira_for_pr(pr_num, title, merge_branches, title_components,
allow_resolved=False):
+def update_jira_for_pr(pr_num, title, merge_branches, title_components):
# asf_jira is guaranteed to be set here: initialize_jira() fails fast
otherwise.
print()
continue_maybe("Would you like to update an associated JIRA?")
@@ -1365,7 +1381,6 @@ def update_jira_for_pr(pr_num, title, merge_branches,
title_components, allow_re
merge_branches,
jira_comment,
title_components,
- allow_resolved=allow_resolved,
)
@@ -1943,9 +1958,7 @@ def main():
# Backport mode may be the first chance to resolve a JIRA after an
interrupted
# original merge. If it was already resolved, add any newly
inferred fix versions.
if not is_minor:
- update_jira_for_pr(
- pr_num, title, picked_refs, title_components,
allow_resolved=True
- )
+ update_jira_for_pr(pr_num, title, picked_refs,
title_components)
sys.exit(0)
if not bool(pr["mergeable"]):
@@ -2001,6 +2014,9 @@ def main():
issue.fields.summary,
issue.fields.status.name,
issue.fields.issuetype.name,
+ resolution=(
+ issue.fields.resolution.name if issue.fields.resolution is
not None else None
+ ),
use_color=True,
is_followup=is_followup,
)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]