commit
6c5a55c466
1 changed files with 369 additions and 0 deletions
@ -0,0 +1,369 @@ |
|||||||
|
#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. |
||||||
|
|
||||||
|
""" |
||||||
|
|
||||||
|
from collections import defaultdict |
||||||
|
import base64 |
||||||
|
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 releases 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 the {version} release ([{name}](https://github.com/grpc/grpc/blob/master/doc/g_stands_for.md)) of gRPC Core. |
||||||
|
|
||||||
|
Please see the notes for the previous releases here: https://github.com/grpc/grpc/releases. Please consult https://grpc.io/ for all information regarding this product. |
||||||
|
|
||||||
|
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 --pretty=online --merges prevRelLabel..relBranch' """ |
||||||
|
|
||||||
|
import subprocess |
||||||
|
print("Running git log --pretty=oneline --merges " + prevRelLabel + ".." + |
||||||
|
relBranch) |
||||||
|
return subprocess.check_output([ |
||||||
|
"git", "log", "--pretty=oneline", "--merges", |
||||||
|
"%s..%s" % (prevRelLabel, relBranch) |
||||||
|
]) |
||||||
|
|
||||||
|
|
||||||
|
def get_pr_data(pr_num): |
||||||
|
"""Get the PR data from github. Return 'error' on exception""" |
||||||
|
|
||||||
|
try: |
||||||
|
from urllib2 import Request, urlopen, HTTPError |
||||||
|
except ImportError: |
||||||
|
import urllib |
||||||
|
from urllib.request import Request, urlopen, HTTPError |
||||||
|
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 |
||||||
|
match = b"Merge pull request #(\d+)" |
||||||
|
prlist = re.findall(match, gitLogs, re.MULTILINE) |
||||||
|
print("\nPRs matching 'Merge pull request #<num>':") |
||||||
|
print(prlist) |
||||||
|
print("\n") |
||||||
|
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 |
||||||
|
prline = prline.encode('ascii', 'ignore') |
||||||
|
detail = detail.encode('ascii', 'ignore') |
||||||
|
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() |
Loading…
Reference in new issue