123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382 |
- #Copyright 2019 gRPC authors.
- #
- # Licensed 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.
- """Generate draft and release notes in Markdown from Github PRs.
- You'll need a github API token to avoid being rate-limited. See
- https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/
- This script collects PRs using "git log X..Y" from local repo where X and Y are
- tags or release branch names of previous and current releases respectively.
- Typically, notes are generated before the release branch is labelled so Y is
- almost always the name of the release branch. X is the previous release branch
- if this is not a patch release. Otherwise, it is the previous release tag.
- For example, for release v1.17.0, X will be origin/v1.16.x and for release v1.17.3,
- X will be v1.17.2. In both cases Y will be origin/v1.17.x.
- """
- import base64
- from collections import defaultdict
- import json
- content_header = """Draft Release Notes For {version}
- --
- Final release notes will be generated from the PR titles that have *"release notes:yes"* label. If you have any additional notes please add them below. These will be appended to auto generated release notes. Previous release notes are [here](https://github.com/grpc/grpc/releases).
- **Also, look at the PRs listed below against your name.** Please apply the missing labels and make necessary corrections (like fixing the title) to the PR in Github. Final release notes will be generated just before the release on {date}.
- Add additional notes not in PRs
- --
- Core
- -
- C++
- -
- C#
- -
- Objective-C
- -
- PHP
- -
- Python
- -
- Ruby
- -
- """
- rl_header = """This is release {version} ([{name}](https://github.com/grpc/grpc/blob/master/doc/g_stands_for.md)) of gRPC Core.
- For gRPC documentation, see [grpc.io](https://grpc.io/). For previous releases, see [Releases](https://github.com/grpc/grpc/releases).
- This release contains refinements, improvements, and bug fixes, with highlights listed below.
- """
- HTML_URL = "https://github.com/grpc/grpc/pull/"
- API_URL = 'https://api.github.com/repos/grpc/grpc/pulls/'
- def get_commit_log(prevRelLabel, relBranch):
- """Return the output of 'git log prevRelLabel..relBranch' """
- import subprocess
- glg_command = [
- "git", "log", "--pretty=oneline", "--committer=GitHub",
- "%s..%s" % (prevRelLabel, relBranch)
- ]
- print(("Running ", " ".join(glg_command)))
- return subprocess.check_output(glg_command).decode('utf-8', 'ignore')
- def get_pr_data(pr_num):
- """Get the PR data from github. Return 'error' on exception"""
- try:
- from urllib.error import HTTPError
- from urllib.request import Request
- from urllib.request import urlopen
- except ImportError:
- import urllib.error
- import urllib.parse
- import urllib.request
- from urllib.request import HTTPError
- from urllib.request import Request
- from urllib.request import urlopen
- url = API_URL + pr_num
- req = Request(url)
- req.add_header('Authorization', 'token %s' % TOKEN)
- try:
- f = urlopen(req)
- response = json.loads(f.read().decode('utf-8'))
- #print(response)
- except HTTPError as e:
- response = json.loads(e.fp.read().decode('utf-8'))
- if 'message' in response:
- print((response['message']))
- response = "error"
- return response
- def get_pr_titles(gitLogs):
- import re
- error_count = 0
- # PRs with merge commits
- match_merge_pr = "Merge pull request #(\d+)"
- prlist_merge_pr = re.findall(match_merge_pr, gitLogs, re.MULTILINE)
- print("\nPRs matching 'Merge pull request #<num>':")
- print(prlist_merge_pr)
- print("\n")
- # PRs using Github's squash & merge feature
- match_sq = "\(#(\d+)\)$"
- prlist_sq = re.findall(match_sq, gitLogs, re.MULTILINE)
- print("\nPRs matching '[PR Description](#<num>)$'")
- print(prlist_sq)
- print("\n")
- prlist = prlist_merge_pr + prlist_sq
- langs_pr = defaultdict(list)
- for pr_num in prlist:
- pr_num = str(pr_num)
- print(("---------- getting data for PR " + pr_num))
- pr = get_pr_data(pr_num)
- if pr == "error":
- print(
- ("\n***ERROR*** Error in getting data for PR " + pr_num + "\n"))
- error_count += 1
- continue
- rl_no_found = False
- rl_yes_found = False
- lang_found = False
- for label in pr['labels']:
- if label['name'] == 'release notes: yes':
- rl_yes_found = True
- elif label['name'] == 'release notes: no':
- rl_no_found = True
- elif label['name'].startswith('lang/'):
- lang_found = True
- lang = label['name'].split('/')[1].lower()
- #lang = lang[0].upper() + lang[1:]
- body = pr["title"]
- if not body.endswith("."):
- body = body + "."
- if not pr["merged_by"]:
- print(("\n***ERROR***: No merge_by found for PR " + pr_num + "\n"))
- error_count += 1
- continue
- prline = "- " + body + " ([#" + pr_num + "](" + HTML_URL + pr_num + "))"
- detail = "- " + pr["merged_by"]["login"] + "@ " + prline
- print(detail)
- #if no RL label
- if not rl_no_found and not rl_yes_found:
- print(("Release notes label missing for " + pr_num))
- langs_pr["nolabel"].append(detail)
- elif rl_yes_found and not lang_found:
- print(("Lang label missing for " + pr_num))
- langs_pr["nolang"].append(detail)
- elif rl_no_found:
- print(("'Release notes:no' found for " + pr_num))
- langs_pr["notinrel"].append(detail)
- elif rl_yes_found:
- print(("'Release notes:yes' found for " + pr_num + " with lang " +
- lang))
- langs_pr["inrel"].append(detail)
- langs_pr[lang].append(prline)
- return langs_pr, error_count
- def write_draft(langs_pr, file, version, date):
- file.write(content_header.format(version=version, date=date))
- file.write("PRs with missing release notes label - please fix in Github\n")
- file.write("---\n")
- file.write("\n")
- if langs_pr["nolabel"]:
- langs_pr["nolabel"].sort()
- file.write("\n".join(langs_pr["nolabel"]))
- else:
- file.write("- None")
- file.write("\n")
- file.write("\n")
- file.write("PRs with missing lang label - please fix in Github\n")
- file.write("---\n")
- file.write("\n")
- if langs_pr["nolang"]:
- langs_pr["nolang"].sort()
- file.write("\n".join(langs_pr["nolang"]))
- else:
- file.write("- None")
- file.write("\n")
- file.write("\n")
- file.write(
- "PRs going into release notes - please check title and fix in Github. Do not edit here.\n"
- )
- file.write("---\n")
- file.write("\n")
- if langs_pr["inrel"]:
- langs_pr["inrel"].sort()
- file.write("\n".join(langs_pr["inrel"]))
- else:
- file.write("- None")
- file.write("\n")
- file.write("\n")
- file.write("PRs not going into release notes\n")
- file.write("---\n")
- file.write("\n")
- if langs_pr["notinrel"]:
- langs_pr["notinrel"].sort()
- file.write("\n".join(langs_pr["notinrel"]))
- else:
- file.write("- None")
- file.write("\n")
- file.write("\n")
- def write_rel_notes(langs_pr, file, version, name):
- file.write(rl_header.format(version=version, name=name))
- if langs_pr["core"]:
- file.write("Core\n---\n\n")
- file.write("\n".join(langs_pr["core"]))
- file.write("\n")
- file.write("\n")
- if langs_pr["c++"]:
- file.write("C++\n---\n\n")
- file.write("\n".join(langs_pr["c++"]))
- file.write("\n")
- file.write("\n")
- if langs_pr["c#"]:
- file.write("C#\n---\n\n")
- file.write("\n".join(langs_pr["c#"]))
- file.write("\n")
- file.write("\n")
- if langs_pr["go"]:
- file.write("Go\n---\n\n")
- file.write("\n".join(langs_pr["go"]))
- file.write("\n")
- file.write("\n")
- if langs_pr["Java"]:
- file.write("Java\n---\n\n")
- file.write("\n".join(langs_pr["Java"]))
- file.write("\n")
- file.write("\n")
- if langs_pr["node"]:
- file.write("Node\n---\n\n")
- file.write("\n".join(langs_pr["node"]))
- file.write("\n")
- file.write("\n")
- if langs_pr["objc"]:
- file.write("Objective-C\n---\n\n")
- file.write("\n".join(langs_pr["objc"]))
- file.write("\n")
- file.write("\n")
- if langs_pr["php"]:
- file.write("PHP\n---\n\n")
- file.write("\n".join(langs_pr["php"]))
- file.write("\n")
- file.write("\n")
- if langs_pr["python"]:
- file.write("Python\n---\n\n")
- file.write("\n".join(langs_pr["python"]))
- file.write("\n")
- file.write("\n")
- if langs_pr["ruby"]:
- file.write("Ruby\n---\n\n")
- file.write("\n".join(langs_pr["ruby"]))
- file.write("\n")
- file.write("\n")
- if langs_pr["other"]:
- file.write("Other\n---\n\n")
- file.write("\n".join(langs_pr["other"]))
- file.write("\n")
- file.write("\n")
- def build_args_parser():
- import argparse
- parser = argparse.ArgumentParser()
- parser.add_argument('release_version',
- type=str,
- help='New release version e.g. 1.14.0')
- parser.add_argument('release_name',
- type=str,
- help='New release name e.g. gladiolus')
- parser.add_argument('release_date',
- type=str,
- help='Release date e.g. 7/30/18')
- parser.add_argument('previous_release_label',
- type=str,
- help='Previous release branch/tag e.g. v1.13.x')
- parser.add_argument('release_branch',
- type=str,
- help='Current release branch e.g. origin/v1.14.x')
- parser.add_argument('draft_filename',
- type=str,
- help='Name of the draft file e.g. draft.md')
- parser.add_argument('release_notes_filename',
- type=str,
- help='Name of the release notes file e.g. relnotes.md')
- parser.add_argument('--token',
- type=str,
- default='',
- help='GitHub API token to avoid being rate limited')
- return parser
- def main():
- import os
- global TOKEN
- parser = build_args_parser()
- args = parser.parse_args()
- version, name, date = args.release_version, args.release_name, args.release_date
- start, end = args.previous_release_label, args.release_branch
- TOKEN = args.token
- if TOKEN == '':
- try:
- TOKEN = os.environ["GITHUB_TOKEN"]
- except:
- pass
- if TOKEN == '':
- print(
- "Error: Github API token required. Either include param --token=<your github token> or set environment variable GITHUB_TOKEN to your github token"
- )
- return
- langs_pr, error_count = get_pr_titles(get_commit_log(start, end))
- draft_file, rel_file = args.draft_filename, args.release_notes_filename
- filename = os.path.abspath(draft_file)
- if os.path.exists(filename):
- file = open(filename, 'r+')
- else:
- file = open(filename, 'w')
- file.seek(0)
- write_draft(langs_pr, file, version, date)
- file.truncate()
- file.close()
- print(("\nDraft notes written to " + filename))
- filename = os.path.abspath(rel_file)
- if os.path.exists(filename):
- file = open(filename, 'r+')
- else:
- file = open(filename, 'w')
- file.seek(0)
- write_rel_notes(langs_pr, file, version, name)
- file.truncate()
- file.close()
- print(("\nRelease notes written to " + filename))
- if error_count > 0:
- print("\n\n*** Errors were encountered. See log. *********\n")
- if __name__ == "__main__":
- main()
|