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]

Reply via email to