14 """Generate draft and release notes in Markdown from Github PRs.
16 You'll need a github API token to avoid being rate-limited. See
17 https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/
19 This script collects PRs using "git log X..Y" from local repo where X and Y are
20 tags or release branch names of previous and current releases respectively.
21 Typically, notes are generated before the release branch is labelled so Y is
22 almost always the name of the release branch. X is the previous release branch
23 if this is not a patch release. Otherwise, it is the previous release tag.
24 For example, for release v1.17.0, X will be origin/v1.16.x and for release v1.17.3,
25 X will be v1.17.2. In both cases Y will be origin/v1.17.x.
29 from collections
import defaultdict
35 logging.basicConfig(level=logging.WARNING)
37 content_header =
"""Draft Release Notes For {version}
39 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).
41 **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}.
43 Add additional notes not in PRs
76 rl_header =
"""This is release {version} ([{name}](https://github.com/grpc/grpc/blob/master/doc/g_stands_for.md)) of gRPC Core.
78 For gRPC documentation, see [grpc.io](https://grpc.io/). For previous releases, see [Releases](https://github.com/grpc/grpc/releases).
80 This release contains refinements, improvements, and bug fixes, with highlights listed below.
85 HTML_URL =
"https://github.com/grpc/grpc/pull/"
86 API_URL =
'https://api.github.com/repos/grpc/grpc/pulls/'
90 """Return the output of 'git log prevRelLabel..relBranch' """
94 "git",
"log",
"--pretty=oneline",
"--committer=GitHub",
95 "%s..%s" % (prevRelLabel, relBranch)
97 print((
"Running ",
" ".join(glg_command)))
98 return subprocess.check_output(glg_command).
decode(
'utf-8',
'ignore')
102 """Get the PR data from github. Return 'error' on exception"""
103 http = urllib3.PoolManager(retries=urllib3.Retry(total=7, backoff_factor=1),
105 url = API_URL + pr_num
107 response = http.request(
'GET',
109 headers={
'Authorization':
'token %s' % TOKEN})
110 except urllib3.exceptions.HTTPError
as e:
111 print(
'Request error:', e.reason)
113 return json.loads(response.data.decode(
'utf-8'))
120 match_merge_pr =
"Merge pull request #(\d+)"
121 prlist_merge_pr = re.findall(match_merge_pr, gitLogs, re.MULTILINE)
122 print(
"\nPRs matching 'Merge pull request #<num>':")
123 print(prlist_merge_pr)
126 match_sq =
"\(#(\d+)\)$"
127 prlist_sq = re.findall(match_sq, gitLogs, re.MULTILINE)
128 print(
"\nPRs matching '[PR Description](#<num>)$'")
131 prlist = prlist_merge_pr + prlist_sq
132 langs_pr = defaultdict(list)
133 for pr_num
in prlist:
135 print((
"---------- getting data for PR " + pr_num))
139 (
"\n***ERROR*** Error in getting data for PR " + pr_num +
"\n"))
145 for label
in pr[
'labels']:
146 if label[
'name'] ==
'release notes: yes':
148 elif label[
'name'] ==
'release notes: no':
150 elif label[
'name'].startswith(
'lang/'):
152 lang = label[
'name'].
split(
'/')[1].lower()
155 if not body.endswith(
"."):
157 if not pr[
"merged_by"]:
158 print((
"\n***ERROR***: No merge_by found for PR " + pr_num +
"\n"))
162 prline =
"- " + body +
" ([#" + pr_num +
"](" + HTML_URL + pr_num +
"))"
163 detail =
"- " + pr[
"merged_by"][
"login"] +
"@ " + prline
166 if not rl_no_found
and not rl_yes_found:
167 print((
"Release notes label missing for " + pr_num))
168 langs_pr[
"nolabel"].append(detail)
169 elif rl_yes_found
and not lang_found:
170 print((
"Lang label missing for " + pr_num))
171 langs_pr[
"nolang"].append(detail)
173 print((
"'Release notes:no' found for " + pr_num))
174 langs_pr[
"notinrel"].append(detail)
176 print((
"'Release notes:yes' found for " + pr_num +
" with lang " +
178 langs_pr[
"inrel"].append(detail)
179 langs_pr[lang].append(prline)
181 return langs_pr, error_count
185 file.write(content_header.format(version=version, date=date))
186 file.write(
"PRs with missing release notes label - please fix in Github\n")
189 if langs_pr[
"nolabel"]:
190 langs_pr[
"nolabel"].sort()
191 file.write(
"\n".join(langs_pr[
"nolabel"]))
196 file.write(
"PRs with missing lang label - please fix in Github\n")
199 if langs_pr[
"nolang"]:
200 langs_pr[
"nolang"].sort()
201 file.write(
"\n".join(langs_pr[
"nolang"]))
207 "PRs going into release notes - please check title and fix in Github. Do not edit here.\n"
211 if langs_pr[
"inrel"]:
212 langs_pr[
"inrel"].sort()
213 file.write(
"\n".join(langs_pr[
"inrel"]))
218 file.write(
"PRs not going into release notes\n")
221 if langs_pr[
"notinrel"]:
222 langs_pr[
"notinrel"].sort()
223 file.write(
"\n".join(langs_pr[
"notinrel"]))
231 file.write(rl_header.format(version=version, name=name))
233 file.write(
"Core\n---\n\n")
234 file.write(
"\n".join(langs_pr[
"core"]))
238 file.write(
"C++\n---\n\n")
239 file.write(
"\n".join(langs_pr[
"c++"]))
243 file.write(
"C#\n---\n\n")
244 file.write(
"\n".join(langs_pr[
"c#"]))
248 file.write(
"Go\n---\n\n")
249 file.write(
"\n".join(langs_pr[
"go"]))
253 file.write(
"Java\n---\n\n")
254 file.write(
"\n".join(langs_pr[
"Java"]))
258 file.write(
"Node\n---\n\n")
259 file.write(
"\n".join(langs_pr[
"node"]))
263 file.write(
"Objective-C\n---\n\n")
264 file.write(
"\n".join(langs_pr[
"objc"]))
268 file.write(
"PHP\n---\n\n")
269 file.write(
"\n".join(langs_pr[
"php"]))
272 if langs_pr[
"python"]:
273 file.write(
"Python\n---\n\n")
274 file.write(
"\n".join(langs_pr[
"python"]))
278 file.write(
"Ruby\n---\n\n")
279 file.write(
"\n".join(langs_pr[
"ruby"]))
282 if langs_pr[
"other"]:
283 file.write(
"Other\n---\n\n")
284 file.write(
"\n".join(langs_pr[
"other"]))
291 parser = argparse.ArgumentParser()
292 parser.add_argument(
'release_version',
294 help=
'New release version e.g. 1.14.0')
295 parser.add_argument(
'release_name',
297 help=
'New release name e.g. gladiolus')
298 parser.add_argument(
'release_date',
300 help=
'Release date e.g. 7/30/18')
301 parser.add_argument(
'previous_release_label',
303 help=
'Previous release branch/tag e.g. v1.13.x')
304 parser.add_argument(
'release_branch',
306 help=
'Current release branch e.g. origin/v1.14.x')
307 parser.add_argument(
'draft_filename',
309 help=
'Name of the draft file e.g. draft.md')
310 parser.add_argument(
'release_notes_filename',
312 help=
'Name of the release notes file e.g. relnotes.md')
313 parser.add_argument(
'--token',
316 help=
'GitHub API token to avoid being rate limited')
325 args = parser.parse_args()
326 version, name, date = args.release_version, args.release_name, args.release_date
327 start, end = args.previous_release_label, args.release_branch
332 TOKEN = os.environ[
"GITHUB_TOKEN"]
337 "Error: Github API token required. Either include param --token=<your github token> or set environment variable GITHUB_TOKEN to your github token"
343 draft_file, rel_file = args.draft_filename, args.release_notes_filename
344 filename = os.path.abspath(draft_file)
345 if os.path.exists(filename):
346 file =
open(filename,
'r+')
348 file =
open(filename,
'w')
354 print((
"\nDraft notes written to " + filename))
356 filename = os.path.abspath(rel_file)
357 if os.path.exists(filename):
358 file =
open(filename,
'r+')
360 file =
open(filename,
'w')
366 print((
"\nRelease notes written to " + filename))
368 print(
"\n\n*** Errors were encountered. See log. *********\n")
371 if __name__ ==
"__main__":