release_notes.py
Go to the documentation of this file.
1 #Copyright 2019 gRPC authors.
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14 """Generate draft and release notes in Markdown from Github PRs.
15 
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/
18 
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.
26 
27 """
28 
29 from collections import defaultdict
30 import json
31 import logging
32 
33 import urllib3
34 
35 logging.basicConfig(level=logging.WARNING)
36 
37 content_header = """Draft Release Notes For {version}
38 --
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).
40 
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}.
42 
43 Add additional notes not in PRs
44 --
45 
46 Core
47 -
48 
49 
50 C++
51 -
52 
53 
54 C#
55 -
56 
57 
58 Objective-C
59 -
60 
61 
62 PHP
63 -
64 
65 
66 Python
67 -
68 
69 
70 Ruby
71 -
72 
73 
74 """
75 
76 rl_header = """This is release {version} ([{name}](https://github.com/grpc/grpc/blob/master/doc/g_stands_for.md)) of gRPC Core.
77 
78 For gRPC documentation, see [grpc.io](https://grpc.io/). For previous releases, see [Releases](https://github.com/grpc/grpc/releases).
79 
80 This release contains refinements, improvements, and bug fixes, with highlights listed below.
81 
82 
83 """
84 
85 HTML_URL = "https://github.com/grpc/grpc/pull/"
86 API_URL = 'https://api.github.com/repos/grpc/grpc/pulls/'
87 
88 
89 def get_commit_log(prevRelLabel, relBranch):
90  """Return the output of 'git log prevRelLabel..relBranch' """
91 
92  import subprocess
93  glg_command = [
94  "git", "log", "--pretty=oneline", "--committer=GitHub",
95  "%s..%s" % (prevRelLabel, relBranch)
96  ]
97  print(("Running ", " ".join(glg_command)))
98  return subprocess.check_output(glg_command).decode('utf-8', 'ignore')
99 
100 
101 def get_pr_data(pr_num):
102  """Get the PR data from github. Return 'error' on exception"""
103  http = urllib3.PoolManager(retries=urllib3.Retry(total=7, backoff_factor=1),
104  timeout=4.0)
105  url = API_URL + pr_num
106  try:
107  response = http.request('GET',
108  url,
109  headers={'Authorization': 'token %s' % TOKEN})
110  except urllib3.exceptions.HTTPError as e:
111  print('Request error:', e.reason)
112  return 'error'
113  return json.loads(response.data.decode('utf-8'))
114 
115 
116 def get_pr_titles(gitLogs):
117  import re
118  error_count = 0
119  # PRs with merge commits
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)
124  print("\n")
125  # PRs using Github's squash & merge feature
126  match_sq = "\(#(\d+)\)$"
127  prlist_sq = re.findall(match_sq, gitLogs, re.MULTILINE)
128  print("\nPRs matching '[PR Description](#<num>)$'")
129  print(prlist_sq)
130  print("\n")
131  prlist = prlist_merge_pr + prlist_sq
132  langs_pr = defaultdict(list)
133  for pr_num in prlist:
134  pr_num = str(pr_num)
135  print(("---------- getting data for PR " + pr_num))
136  pr = get_pr_data(pr_num)
137  if pr == "error":
138  print(
139  ("\n***ERROR*** Error in getting data for PR " + pr_num + "\n"))
140  error_count += 1
141  continue
142  rl_no_found = False
143  rl_yes_found = False
144  lang_found = False
145  for label in pr['labels']:
146  if label['name'] == 'release notes: yes':
147  rl_yes_found = True
148  elif label['name'] == 'release notes: no':
149  rl_no_found = True
150  elif label['name'].startswith('lang/'):
151  lang_found = True
152  lang = label['name'].split('/')[1].lower()
153  #lang = lang[0].upper() + lang[1:]
154  body = pr["title"]
155  if not body.endswith("."):
156  body = body + "."
157  if not pr["merged_by"]:
158  print(("\n***ERROR***: No merge_by found for PR " + pr_num + "\n"))
159  error_count += 1
160  continue
161 
162  prline = "- " + body + " ([#" + pr_num + "](" + HTML_URL + pr_num + "))"
163  detail = "- " + pr["merged_by"]["login"] + "@ " + prline
164  print(detail)
165  #if no RL label
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)
172  elif rl_no_found:
173  print(("'Release notes:no' found for " + pr_num))
174  langs_pr["notinrel"].append(detail)
175  elif rl_yes_found:
176  print(("'Release notes:yes' found for " + pr_num + " with lang " +
177  lang))
178  langs_pr["inrel"].append(detail)
179  langs_pr[lang].append(prline)
180 
181  return langs_pr, error_count
182 
183 
184 def write_draft(langs_pr, file, version, date):
185  file.write(content_header.format(version=version, date=date))
186  file.write("PRs with missing release notes label - please fix in Github\n")
187  file.write("---\n")
188  file.write("\n")
189  if langs_pr["nolabel"]:
190  langs_pr["nolabel"].sort()
191  file.write("\n".join(langs_pr["nolabel"]))
192  else:
193  file.write("- None")
194  file.write("\n")
195  file.write("\n")
196  file.write("PRs with missing lang label - please fix in Github\n")
197  file.write("---\n")
198  file.write("\n")
199  if langs_pr["nolang"]:
200  langs_pr["nolang"].sort()
201  file.write("\n".join(langs_pr["nolang"]))
202  else:
203  file.write("- None")
204  file.write("\n")
205  file.write("\n")
206  file.write(
207  "PRs going into release notes - please check title and fix in Github. Do not edit here.\n"
208  )
209  file.write("---\n")
210  file.write("\n")
211  if langs_pr["inrel"]:
212  langs_pr["inrel"].sort()
213  file.write("\n".join(langs_pr["inrel"]))
214  else:
215  file.write("- None")
216  file.write("\n")
217  file.write("\n")
218  file.write("PRs not going into release notes\n")
219  file.write("---\n")
220  file.write("\n")
221  if langs_pr["notinrel"]:
222  langs_pr["notinrel"].sort()
223  file.write("\n".join(langs_pr["notinrel"]))
224  else:
225  file.write("- None")
226  file.write("\n")
227  file.write("\n")
228 
229 
230 def write_rel_notes(langs_pr, file, version, name):
231  file.write(rl_header.format(version=version, name=name))
232  if langs_pr["core"]:
233  file.write("Core\n---\n\n")
234  file.write("\n".join(langs_pr["core"]))
235  file.write("\n")
236  file.write("\n")
237  if langs_pr["c++"]:
238  file.write("C++\n---\n\n")
239  file.write("\n".join(langs_pr["c++"]))
240  file.write("\n")
241  file.write("\n")
242  if langs_pr["c#"]:
243  file.write("C#\n---\n\n")
244  file.write("\n".join(langs_pr["c#"]))
245  file.write("\n")
246  file.write("\n")
247  if langs_pr["go"]:
248  file.write("Go\n---\n\n")
249  file.write("\n".join(langs_pr["go"]))
250  file.write("\n")
251  file.write("\n")
252  if langs_pr["Java"]:
253  file.write("Java\n---\n\n")
254  file.write("\n".join(langs_pr["Java"]))
255  file.write("\n")
256  file.write("\n")
257  if langs_pr["node"]:
258  file.write("Node\n---\n\n")
259  file.write("\n".join(langs_pr["node"]))
260  file.write("\n")
261  file.write("\n")
262  if langs_pr["objc"]:
263  file.write("Objective-C\n---\n\n")
264  file.write("\n".join(langs_pr["objc"]))
265  file.write("\n")
266  file.write("\n")
267  if langs_pr["php"]:
268  file.write("PHP\n---\n\n")
269  file.write("\n".join(langs_pr["php"]))
270  file.write("\n")
271  file.write("\n")
272  if langs_pr["python"]:
273  file.write("Python\n---\n\n")
274  file.write("\n".join(langs_pr["python"]))
275  file.write("\n")
276  file.write("\n")
277  if langs_pr["ruby"]:
278  file.write("Ruby\n---\n\n")
279  file.write("\n".join(langs_pr["ruby"]))
280  file.write("\n")
281  file.write("\n")
282  if langs_pr["other"]:
283  file.write("Other\n---\n\n")
284  file.write("\n".join(langs_pr["other"]))
285  file.write("\n")
286  file.write("\n")
287 
288 
290  import argparse
291  parser = argparse.ArgumentParser()
292  parser.add_argument('release_version',
293  type=str,
294  help='New release version e.g. 1.14.0')
295  parser.add_argument('release_name',
296  type=str,
297  help='New release name e.g. gladiolus')
298  parser.add_argument('release_date',
299  type=str,
300  help='Release date e.g. 7/30/18')
301  parser.add_argument('previous_release_label',
302  type=str,
303  help='Previous release branch/tag e.g. v1.13.x')
304  parser.add_argument('release_branch',
305  type=str,
306  help='Current release branch e.g. origin/v1.14.x')
307  parser.add_argument('draft_filename',
308  type=str,
309  help='Name of the draft file e.g. draft.md')
310  parser.add_argument('release_notes_filename',
311  type=str,
312  help='Name of the release notes file e.g. relnotes.md')
313  parser.add_argument('--token',
314  type=str,
315  default='',
316  help='GitHub API token to avoid being rate limited')
317  return parser
318 
319 
320 def main():
321  import os
322  global TOKEN
323 
324  parser = build_args_parser()
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
328 
329  TOKEN = args.token
330  if TOKEN == '':
331  try:
332  TOKEN = os.environ["GITHUB_TOKEN"]
333  except:
334  pass
335  if TOKEN == '':
336  print(
337  "Error: Github API token required. Either include param --token=<your github token> or set environment variable GITHUB_TOKEN to your github token"
338  )
339  return
340 
341  langs_pr, error_count = get_pr_titles(get_commit_log(start, end))
342 
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+')
347  else:
348  file = open(filename, 'w')
349 
350  file.seek(0)
351  write_draft(langs_pr, file, version, date)
352  file.truncate()
353  file.close()
354  print(("\nDraft notes written to " + filename))
355 
356  filename = os.path.abspath(rel_file)
357  if os.path.exists(filename):
358  file = open(filename, 'r+')
359  else:
360  file = open(filename, 'w')
361 
362  file.seek(0)
363  write_rel_notes(langs_pr, file, version, name)
364  file.truncate()
365  file.close()
366  print(("\nRelease notes written to " + filename))
367  if error_count > 0:
368  print("\n\n*** Errors were encountered. See log. *********\n")
369 
370 
371 if __name__ == "__main__":
372  main()
xds_interop_client.str
str
Definition: xds_interop_client.py:487
release_notes.main
def main()
Definition: release_notes.py:320
release_notes.write_draft
def write_draft(langs_pr, file, version, date)
Definition: release_notes.py:184
release_notes.build_args_parser
def build_args_parser()
Definition: release_notes.py:289
release_notes.get_pr_titles
def get_pr_titles(gitLogs)
Definition: release_notes.py:116
release_notes.write_rel_notes
def write_rel_notes(langs_pr, file, version, name)
Definition: release_notes.py:230
main
Definition: main.py:1
grpc._common.decode
def decode(b)
Definition: grpc/_common.py:75
open
#define open
Definition: test-fs.c:46
split
static void split(const char *s, char ***ss, size_t *ns)
Definition: debug/trace.cc:111
release_notes.get_commit_log
def get_commit_log(prevRelLabel, relBranch)
Definition: release_notes.py:89
release_notes.get_pr_data
def get_pr_data(pr_num)
Definition: release_notes.py:101


grpc
Author(s):
autogenerated on Thu Mar 13 2025 03:01:08