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 d40de7a6df74 [SPARK-57341][INFRA] Reconcile JIRA components with the
PR title in merge script
d40de7a6df74 is described below
commit d40de7a6df74826c5bfe1b4c2e35ab7b844e7294
Author: Ruifeng Zheng <[email protected]>
AuthorDate: Tue Jun 30 15:07:41 2026 +0800
[SPARK-57341][INFRA] Reconcile JIRA components with the PR title in merge
script
### What changes were proposed in this pull request?
This PR adds a component-reconciliation step to `dev/merge_spark_pr.py`.
When a PR is merged, the script already normalizes the component tags in the PR
title (e.g. `[SQL]`, `[CORE]`, `[TEST]`). This change maps every title tag that
corresponds to a JIRA component -- primary or not, e.g. `[SQL]` -> "SQL" and
`[TEST]` -> "Tests" -- and compares that set against the components on the
linked JIRA ticket. On a mismatch it prompts the committer to:
- **overwrite** the JIRA ticket's components with the PR title's,
- **append** the PR title's components to the ticket, or
- **keep** the JIRA ticket unchanged (the default).
Tags that do not correspond to a JIRA component (`[MINOR]`, `[FOLLOWUP]`,
version tags like `[4.X]`, unknown tags) are ignored. The JIRA issue summary
printed during a merge now also lists the ticket's components.
### Why are the changes needed?
The PR title and the JIRA ticket can drift out of sync on which components
a change touches. Today the merge tool resolves the ticket without checking, so
a committer has to notice and fix component mismatches by hand. Surfacing the
difference at merge time, with a safe default of leaving JIRA untouched, makes
it easy to keep the two consistent without forcing any change.
### Does this PR introduce _any_ user-facing change?
No. `dev/merge_spark_pr.py` is a committer-only tool.
### How was this patch tested?
The script runs its doctests on startup via `doctest.testmod()`. A doctest
covering the tag-to-JIRA-component mapping (including non-primary tags such as
`[TEST]` -> "Tests") was added; the full suite passes. Formatting was verified
with `black 26.3.1` (the repo's pinned version) against the root
`pyproject.toml`.
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 4.8)
Closes #56400 from zhengruifeng/merge-jira-component-reconcile.
Authored-by: Ruifeng Zheng <[email protected]>
Signed-off-by: Ruifeng Zheng <[email protected]>
---
dev/merge_spark_pr.py | 97 +++++++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 91 insertions(+), 6 deletions(-)
diff --git a/dev/merge_spark_pr.py b/dev/merge_spark_pr.py
index fe40766add05..9e44e5fd84c6 100755
--- a/dev/merge_spark_pr.py
+++ b/dev/merge_spark_pr.py
@@ -652,13 +652,91 @@ 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
+ 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]
fix_versions = ""
if len(issue.fields.fixVersions) > 0:
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" % (summary, assignee, status, url, target_versions,
fix_versions))
+ print(
+ "%s%s%s%s%s%s%s"
+ % (summary, assignee, status, components, url, target_versions,
fix_versions)
+ )
+
+
+def jira_components_from_title_tags(tags):
+ """Canonical SPARK JIRA component names implied by PR-title component tags.
+
+ Each tag is resolved through the component registry; a tag that maps to a
+ JIRA component contributes that component's canonical name, whether primary
+ or not (e.g. [SQL] -> "SQL", [TEST] -> "Tests"). Tags that are not JIRA
+ components (status markers like [FOLLOWUP]/[MINOR], version tags like
[4.X],
+ or unknown tags) contribute nothing. Aliases normalize to the canonical
+ name. The result preserves input order and is de-duplicated.
+
+ >>> jira_components_from_title_tags(["SQL", "CORE"])
+ ['SQL', 'Spark Core']
+ >>> jira_components_from_title_tags(["PYTHON", "DOCS"])
+ ['PySpark', 'Documentation']
+ >>> jira_components_from_title_tags(["SQL", "TEST"])
+ ['SQL', 'Tests']
+ >>> jira_components_from_title_tags(["SQL", "FOLLOWUP", "4.X", "BOGUS"])
+ ['SQL']
+ >>> jira_components_from_title_tags(["SQL", "SQL"])
+ ['SQL']
+ """
+ names = []
+ for tag in tags:
+ c = Component.find(tag)
+ if c is not None and c.jira_name:
+ names.append(c.jira_name)
+ return list(dict.fromkeys(names))
+
+
+def reconcile_jira_components(issue, title_components):
+ """Prompt to sync JIRA components when they differ from the PR title.
+
+ ``title_components`` is the list of normalized PR-title component tags
(e.g.
+ ["SQL", "TEST"]). Every tag that maps to a JIRA component -- primary or
not,
+ e.g. [SQL] -> "SQL" and [TEST] -> "Tests" -- is reconciled; tags with no
JIRA
+ component ([MINOR], [FOLLOWUP], version tags, unknown tags) are dropped.
The
+ mapped names are compared, as a set, against the issue's current
components.
+ On a mismatch, offer to overwrite JIRA with the PR title's components,
append
+ them to the existing ones, or keep JIRA unchanged (the default).
+ """
+ title_jira_components = jira_components_from_title_tags(title_components)
+ if not title_jira_components:
+ return
+ current = [c.name for c in issue.fields.components]
+ if set(current) == set(title_jira_components):
+ return
+
+ print()
+ print("=" * 80)
+ print("PR title components differ from JIRA %s:" % issue.key)
+ print(" PR title: %s" % ", ".join(title_jira_components))
+ print(" JIRA: %s" % (", ".join(current) if current else "(none)"))
+ print("=" * 80)
+ choice = get_input(
+ "[o]verwrite JIRA with PR title / [a]ppend PR title to JIRA / [k]eep
JIRA as is "
+ "(default: keep): ",
+ {"o": ["o", "overwrite"], "a": ["a", "append"], "k": ["k", "keep",
""]},
+ )
+ if choice == "k":
+ print("Keeping JIRA %s components unchanged." % issue.key)
+ return
+ if choice == "o":
+ new_names = list(title_jira_components)
+ elif choice == "a":
+ # Append the PR title's components, keeping the existing ones first.
+ new_names = list(dict.fromkeys(current + title_jira_components))
+
+ try:
+ issue.update(fields={"components": [{"name": n} for n in new_names]})
+ print("Updated JIRA %s components to: %s" % (issue.key, ",
".join(new_names)))
+ except Exception as e:
+ print_error("Failed to update components on JIRA %s: %s" % (issue.key,
e))
def get_jira_issue(prompt, default_jira_id=""):
@@ -684,7 +762,7 @@ def get_jira_issue(prompt, default_jira_id=""):
return get_jira_issue("Enter the revised JIRA ID again or leave blank
to skip")
-def resolve_jira_issue(merge_branches, comment, default_jira_id=""):
+def resolve_jira_issue(merge_branches, comment, default_jira_id="",
title_components=()):
issue = get_jira_issue("Enter a JIRA id", default_jira_id)
if issue is None:
return
@@ -692,6 +770,8 @@ def resolve_jira_issue(merge_branches, comment,
default_jira_id=""):
if issue.fields.assignee is None:
choose_jira_assignee(issue)
+ reconcile_jira_components(issue, title_components)
+
versions = asf_jira.project_versions("SPARK")
# Consider only x.y.z, unreleased, unarchived versions
versions = [
@@ -830,13 +910,13 @@ def assign_issue(issue: int, assignee: str) -> bool:
return True
-def resolve_jira_issues(title, merge_branches, comment):
+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:
- resolve_jira_issue(merge_branches, comment)
+ resolve_jira_issue(merge_branches, comment,
title_components=title_components)
for jira_id in jira_ids:
- resolve_jira_issue(merge_branches, comment, jira_id)
+ resolve_jira_issue(merge_branches, comment, jira_id,
title_components=title_components)
class Component:
@@ -1248,6 +1328,10 @@ def main():
# e.g. 'Reapply "[SPARK-56357][BUILD] Upgrade sbt to 1.12.8"'
is_reapply_pr = title.startswith('Reapply "') and title.endswith('"')
+ # Normalized PR-title component tags, used later to reconcile JIRA
components. Empty for
+ # Revert/Reapply PRs, whose titles are kept verbatim and not parsed for
components.
+ title_components: List[str] = []
+
# Revert and Reapply PRs keep their title verbatim.
if not (is_revert_pr or is_reapply_pr):
# Parse; fail on a malformed title.
@@ -1290,6 +1374,7 @@ def main():
print_error("Title has unknown tag(s): %s" % ", ".join("[%s]" % t
for t in unknown))
parsed.components = components
+ title_components = list(parsed.components)
title = str(parsed)
if title != pr["title"]:
print("Normalized title: %s" % title)
@@ -1451,7 +1536,7 @@ def main():
GITHUB_BASE,
pr_num,
)
- resolve_jira_issues(title, merged_refs, jira_comment)
+ resolve_jira_issues(title, merged_refs, jira_comment, title_components)
if __name__ == "__main__":
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]