| 
									
										
										
										
											2024-07-05 20:01:13 +08:00
										 |  |  | # | 
					
						
							|  |  |  | # Licensed to the Apache Software Foundation (ASF) under one or more | 
					
						
							|  |  |  | # contributor license agreements.  See the NOTICE file distributed with | 
					
						
							|  |  |  | # this work for additional information regarding copyright ownership. | 
					
						
							|  |  |  | # The ASF licenses this file to You under the Apache License, Version 2.0 | 
					
						
							|  |  |  | # (the "License"); you may not use this file except in compliance with | 
					
						
							|  |  |  | # the License.  You may obtain a copy of the License at | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | #    http://www.apache.org/licenses/LICENSE-2.0 | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | # Unless required by applicable law or agreed to in writing, software | 
					
						
							|  |  |  | # distributed under the License is distributed on an "AS IS" BASIS, | 
					
						
							|  |  |  | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
					
						
							|  |  |  | # See the License for the specific language governing permissions and | 
					
						
							|  |  |  | # limitations under the License. | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | """
 | 
					
						
							|  |  |  | Usage: python notes.py <version> > RELEASE_NOTES.html | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Generates release notes for a release in HTML format containing | 
					
						
							|  |  |  | introductory information about the release with links to the | 
					
						
							|  |  |  | Kafka docs and the list of issues resolved in the release. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The script will fail if there are any unresolved issues still | 
					
						
							|  |  |  | marked with the target release. This script should be run after either | 
					
						
							|  |  |  | resolving all issues or moving outstanding issues to a later release. | 
					
						
							|  |  |  | """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | from jira import JIRA | 
					
						
							|  |  |  | import itertools, sys | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | JIRA_BASE_URL = 'https://issues.apache.org/jira' | 
					
						
							|  |  |  | MAX_RESULTS = 100 # This is constrained for cloud instances so we need to fix this value | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def query(query, **kwargs): | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     Fetch all issues matching the JQL query from JIRA and expand paginated results. | 
					
						
							|  |  |  |     Any additional keyword arguments are forwarded to jira.search_issues. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     results = [] | 
					
						
							| 
									
										
										
										
											2025-08-26 15:57:49 +08:00
										 |  |  |     start_at = 0 | 
					
						
							| 
									
										
										
										
											2024-07-05 20:01:13 +08:00
										 |  |  |     new_results = None | 
					
						
							|  |  |  |     jira = JIRA(JIRA_BASE_URL) | 
					
						
							|  |  |  |     while new_results is None or len(new_results) == MAX_RESULTS: | 
					
						
							| 
									
										
										
										
											2025-08-26 15:57:49 +08:00
										 |  |  |         new_results = jira.search_issues(query, startAt=start_at, maxResults=MAX_RESULTS, **kwargs) | 
					
						
							| 
									
										
										
										
											2024-07-05 20:01:13 +08:00
										 |  |  |         results += new_results | 
					
						
							| 
									
										
										
										
											2025-08-26 15:57:49 +08:00
										 |  |  |         start_at += len(new_results) | 
					
						
							| 
									
										
										
										
											2024-07-05 20:01:13 +08:00
										 |  |  |     return results | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def filter_unresolved(issues): | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     Some resolutions, including a lack of resolution, indicate that | 
					
						
							|  |  |  |     the bug hasn't actually been addressed and we shouldn't even | 
					
						
							|  |  |  |     be able to create a release until they are fixed | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     UNRESOLVED_RESOLUTIONS = [None, | 
					
						
							|  |  |  |                               "Unresolved", | 
					
						
							|  |  |  |                               "Duplicate", | 
					
						
							|  |  |  |                               "Invalid", | 
					
						
							|  |  |  |                               "Not A Problem", | 
					
						
							|  |  |  |                               "Not A Bug", | 
					
						
							|  |  |  |                               "Won't Fix", | 
					
						
							|  |  |  |                               "Incomplete", | 
					
						
							|  |  |  |                               "Cannot Reproduce", | 
					
						
							|  |  |  |                               "Later", | 
					
						
							|  |  |  |                               "Works for Me", | 
					
						
							|  |  |  |                               "Workaround", | 
					
						
							|  |  |  |                               "Information Provided" | 
					
						
							|  |  |  |                               ] | 
					
						
							|  |  |  |     return [issue for issue in issues if issue.fields.resolution in UNRESOLVED_RESOLUTIONS or issue.fields.resolution.name in UNRESOLVED_RESOLUTIONS] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def issue_link(issue): | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     Generates a link to the specified JIRA issue. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     return f"{JIRA_BASE_URL}/browse/{issue.key}" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def render(version, issues): | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     Renders the release notes HTML with the given version and issues. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     base_url = "https://kafka.apache.org/" | 
					
						
							|  |  |  |     docs_path = "documentation.html" | 
					
						
							|  |  |  |     minor_version_dotless = "".join(version.split(".")[:2]) # i.e., 10 if version == 1.0.1 | 
					
						
							|  |  |  |     def issue_type_key(issue): | 
					
						
							|  |  |  |         if issue.fields.issuetype.name == 'New Feature': | 
					
						
							|  |  |  |             return -2 | 
					
						
							|  |  |  |         if issue.fields.issuetype.name == 'Improvement': | 
					
						
							|  |  |  |             return -1 | 
					
						
							|  |  |  |         return int(issue.fields.issuetype.id) | 
					
						
							|  |  |  |     by_group = [(k,sorted(g, key=lambda issue: issue.id)) for k,g in itertools.groupby(sorted(issues, key=issue_type_key), lambda issue: issue.fields.issuetype.name)] | 
					
						
							|  |  |  |     parts = [f"""
 | 
					
						
							|  |  |  | <h1>Release Notes - Kafka - Version {version}</h1> | 
					
						
							|  |  |  | <p> | 
					
						
							|  |  |  |     Below is a summary of the JIRA issues addressed in the {version} | 
					
						
							|  |  |  |     release of Kafka. For full documentation of the release, a guide | 
					
						
							|  |  |  |     to get started, and information about the project, see the | 
					
						
							|  |  |  |     <a href="{base_url}">Kafka project site</a>. | 
					
						
							|  |  |  | </p> | 
					
						
							|  |  |  | <p> | 
					
						
							|  |  |  |     <b>Note about upgrades:</b> Please carefully review the | 
					
						
							|  |  |  |     <a href="{base_url}{minor_version_dotless}/{docs_path}#upgrade"> | 
					
						
							|  |  |  |     upgrade documentation</a> for this release thoroughly before upgrading | 
					
						
							|  |  |  |     your cluster. The upgrade notes discuss any critical information about | 
					
						
							|  |  |  |     incompatibilities and breaking changes, performance changes, and any | 
					
						
							|  |  |  |     other changes that might impact your production deployment of Kafka. | 
					
						
							|  |  |  | </p> | 
					
						
							|  |  |  | <p> | 
					
						
							|  |  |  |     The documentation for the most recent release can be found at | 
					
						
							|  |  |  |     <a href="{base_url}{docs_path}">{base_url}{docs_path}</a>. | 
					
						
							|  |  |  | </p> | 
					
						
							|  |  |  | """]
 | 
					
						
							|  |  |  |     for itype, issues in by_group: | 
					
						
							|  |  |  |         parts.append(f"<h2>{itype}</h2>") | 
					
						
							|  |  |  |         parts.append("</ul>") | 
					
						
							|  |  |  |         for issue in issues: | 
					
						
							|  |  |  |             link = issue_link(issue) | 
					
						
							|  |  |  |             key = issue.key | 
					
						
							|  |  |  |             summary = issue.fields.summary | 
					
						
							|  |  |  |             parts.append(f"<li>[<a href=\"{link}\">{key}</a>] - {summary}</li>") | 
					
						
							|  |  |  |         parts.append("</ul>") | 
					
						
							|  |  |  |     return "\n".join(parts) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def issue_str(issue): | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     Provides a human readable string representation for the given issue. | 
					
						
							|  |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2024-07-17 22:45:32 +08:00
										 |  |  |     key = "%15s" % issue.key | 
					
						
							|  |  |  |     resolution = "%15s" % issue.fields.resolution | 
					
						
							| 
									
										
										
										
											2024-07-05 20:01:13 +08:00
										 |  |  |     link = issue_link(issue) | 
					
						
							| 
									
										
										
										
											2024-07-17 22:45:32 +08:00
										 |  |  |     return f"{key} {resolution} {link}" | 
					
						
							| 
									
										
										
										
											2024-07-05 20:01:13 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | def generate(version): | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     Generates the release notes in HTML format for given version. | 
					
						
							|  |  |  |     Raises an error if there are unresolved issues or no issues | 
					
						
							|  |  |  |     at all for the specified version. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     issues = query(f"project=KAFKA and fixVersion={version}") | 
					
						
							|  |  |  |     if not issues: | 
					
						
							|  |  |  |         raise Exception(f"Didn't find any issues for version {version}") | 
					
						
							|  |  |  |     unresolved_issues = filter_unresolved(issues) | 
					
						
							|  |  |  |     if unresolved_issues: | 
					
						
							|  |  |  |         issue_list = "\n".join([issue_str(issue) for issue in unresolved_issues]) | 
					
						
							|  |  |  |         raise Exception(f"""
 | 
					
						
							|  |  |  | Release {version} is not complete since there are unresolved or improperly | 
					
						
							|  |  |  | resolved issues tagged {version} as the fix version: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | {issue_list} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Note that for some resolutions, you should simply remove the fix version | 
					
						
							|  |  |  | as they have not been truly fixed in this release. | 
					
						
							|  |  |  |         """)
 | 
					
						
							|  |  |  |     return render(version, issues) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | if __name__ == "__main__": | 
					
						
							|  |  |  |     if len(sys.argv) != 2: | 
					
						
							|  |  |  |         print("Usage: python notes.py <version>", file=sys.stderr) | 
					
						
							|  |  |  |         sys.exit(1) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     version = sys.argv[1] | 
					
						
							|  |  |  |     try: | 
					
						
							|  |  |  |        print(generate(version)) | 
					
						
							|  |  |  |     except Exception as e: | 
					
						
							|  |  |  |         print(e, file=sys.stderr) | 
					
						
							|  |  |  |         sys.exit(1) |