| # |
| # 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 = [] |
| startAt = 0 |
| new_results = None |
| jira = JIRA(JIRA_BASE_URL) |
| while new_results is None or len(new_results) == MAX_RESULTS: |
| new_results = jira.search_issues(query, startAt=startAt, maxResults=MAX_RESULTS, **kwargs) |
| results += new_results |
| startAt += len(new_results) |
| 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. |
| """ |
| key = "%15s" % issue.key |
| resolution = "%15s" % issue.fields.resolution |
| link = issue_link(issue) |
| return f"{key} {resolution} {link}" |
| |
| 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) |
| |
| |