hurl/bin/release/get_release_note.py

263 lines
8.2 KiB
Python
Executable File

#!/usr/bin/env python3
"""Create Release note from GitHub Issues and Pull Requests for a given version
Example:
$ python3 bin/release/get_release_note.py 1.7.0
"""
import argparse
import datetime
import json
import sys
from typing import List, Optional
import requests
hurl_repo_url = "https://github.com/Orange-OpenSource/hurl"
class Pull:
def __init__(
self,
url: str,
description: str,
author: str,
tags: Optional[List[str]] = None,
issues: Optional[List[int]] = None,
):
if tags is None:
tags = []
if issues is None:
issues = []
self.url = url
self.description = description
self.author = author
self.tags = tags
self.issues = issues
def __repr__(self):
return 'Pull("%s", "%s", "%s", "%s", %s)' % (
self.url,
self.description,
self.author,
str(self.tags),
str(self.issues),
)
def __eq__(self, other):
"""Overrides the default implementation"""
if isinstance(other, Pull):
if self.url != other.url:
return False
if self.description != other.description:
return False
if self.author != other.author:
return False
if self.tags != other.tags:
return False
if self.issues != other.issues:
return False
return True
return False
class Issue:
def __init__(self, number: int, tags: List[str], author: str, pulls: List[Pull]):
self.number = number
self.tags = tags
self.author = author
self.pulls = pulls
def __repr__(self):
return (
'Issue(\n number=%s,\n tag=["%s"],\n author="%s",\n pulls=[%s]\n)'
% (
self.number,
",".join(['"%s"' % t for t in self.tags]),
self.author,
",".join([str(p) for p in self.pulls]),
)
)
def release_note(milestone: str, token: Optional[str]) -> str:
"""return markdown release note for the given milestone"""
date = datetime.datetime.now()
query = """\
query {
repository(owner:"Orange-OpenSource", name:"hurl") {
milestones(query:"MILESTONE", first:1) {
edges {
node {
issues(last:100, states:CLOSED) {
edges {
node {
title
number
url
author {
login
}
closedByPullRequestsReferences(includeClosedPrs:true, first:5) {
edges {
node {
title
url
author {
login
}
}
}
}
labels(first:5) {
edges {
node {
name
}
}
}
}
}
}
}
}
}
}
}
"""
query = query.replace("MILESTONE", milestone)
payload = github_graphql(token=token, query=query)
response = json.loads(payload)
issues_dict = response["data"]["repository"]["milestones"]["edges"][0]["node"][
"issues"
]["edges"]
issues = []
for issue_dict in issues_dict:
number = issue_dict["node"]["number"]
author_issue = issue_dict["node"]["author"]["login"]
tags_dict = issue_dict["node"]["labels"]["edges"]
tags = [t["node"]["name"] for t in tags_dict]
pulls = []
pulls_dict = issue_dict["node"]["closedByPullRequestsReferences"]["edges"]
for pull_dict in pulls_dict:
title = pull_dict["node"]["title"]
url = pull_dict["node"]["url"]
author_pull = pull_dict["node"]["author"]["login"]
pull = Pull(description=title, url=url, author=author_pull)
pulls.append(pull)
issue = Issue(number=number, tags=tags, author=author_issue, pulls=pulls)
issues.append(issue)
pulls = pulls_from_issues(issues)
authors = [
author
for author in authors_from_issues(issues)
if author not in ["jcamiel", "lepapareil", "fabricereix"]
]
return generate_md(milestone, date, pulls, authors)
def pulls_from_issues(issues: List[Issue]) -> List[Pull]:
"""return list of pulls from list of issues"""
pulls: dict[str, Pull] = {}
for issue in issues:
for pull in issue.pulls:
if pull.url in pulls:
saved_pull = pulls[pull.url]
for tag in issue.tags:
if tag not in saved_pull.tags:
saved_pull.tags.append(tag)
saved_pull.issues.append(issue.number)
else:
if pull.url.startswith("https://github.com/Orange-OpenSource/hurl"):
pull.tags = issue.tags
pull.issues.append(issue.number)
pulls[pull.url] = pull
return list(pulls.values())
def authors_from_issues(issues: List[Issue]) -> List[str]:
"""return list of unique authors from a list of issues"""
authors = []
for issue in issues:
if issue.author not in authors:
authors.append(issue.author)
for pull in issue.pulls:
if pull.author not in authors:
authors.append(pull.author)
return authors
def generate_md(
milestone: str, date: datetime.datetime, pulls: List[Pull], authors: List[str]
) -> str:
"""Generate Markdown"""
s = "[%s (%s)](%s)" % (
milestone,
date.strftime("%Y-%m-%d"),
hurl_repo_url + "/blob/master/CHANGELOG.md#" + milestone,
)
s += "\n========================================================================================================================"
s += "\n\nThanks to"
for author in authors:
s += "\n[@%s](https://github.com/%s)," % (author, author)
categories = {
"breaking": "Breaking Changes",
"enhancement": "Enhancements",
"bug": "Bugs Fixed",
"security": "Security Issues Fixed",
"deprecation": "Deprecations",
}
for category in categories:
category_pulls = [pull for pull in pulls if category in pull.tags]
if len(category_pulls) > 0:
s += "\n\n" + categories[category] + ":" + "\n\n"
for pull in category_pulls:
issues = " ".join(
"[#%s](%s/issues/%s)" % (issue, hurl_repo_url, issue)
for issue in pull.issues
)
s += "* %s %s\n" % (pull.description, issues)
s += "\n"
return s
def github_graphql(token: Optional[str], query: str) -> str:
"""Execute a GraphQL query using GitHub API."""
url = "https://api.github.com/graphql"
query_json = {"query": query}
body = json.dumps(query_json)
sys.stderr.write("* POST %s\n" % url)
headers = {}
if token:
headers["Authorization"] = f"Bearer {token}"
r = requests.post(url, data=body, headers=headers)
if r.status_code != 200:
raise Exception("HTTP Error %s - %s" % (r.status_code, r.text))
return r.text
def main():
parser = argparse.ArgumentParser(
description="Get Hurl release notes from issues/PR"
)
parser.add_argument("version", help="Hurl release version ex 4.2.0")
parser.add_argument("--token", help="GitHub authentication token")
args = parser.parse_args()
if args.version == "":
raise Exception("version can not be empty")
print(release_note(milestone=args.version, token=args.token))
if __name__ == "__main__":
main()