| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377 | #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. Seehttps://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 aretags or release branch names of previous and current releases respectively.Typically, notes are generated before the release branch is labelled so Y isalmost always the name of the release branch. X is the previous release branchif 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 defaultdictimport base64import jsoncontent_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 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)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 responsedef get_pr_titles(gitLogs):    import re    error_count = 0    # PRs with merge commits    match_merge_pr = b"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 = b"\(#(\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        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_countdef 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 parserdef 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()
 |