From e645d9ffd5bf5d52616aadd85dfaefab3a0cbad0 Mon Sep 17 00:00:00 2001 From: Anas Nashif Date: Fri, 4 Oct 2019 10:41:07 -0400 Subject: [PATCH] scripts: add script for listing closed bugs Script to be used when creating a release. For regular releases this can be called this way: $ list_issues.py -f issues.md -s 2019-09-01 Will list all closed issues since september 1st, 2019 and will create a markdown file with all issues that can be added as is to the release notes. Signed-off-by: Anas Nashif --- CODEOWNERS | 1 + scripts/release/list_issues.py | 217 +++++++++++++++++++++++++++++++++ 2 files changed, 218 insertions(+) create mode 100755 scripts/release/list_issues.py diff --git a/CODEOWNERS b/CODEOWNERS index ff75be87ff..99a3411d6c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -326,6 +326,7 @@ /scripts/sanity_chk/expr_parser.py @nashif /scripts/gen_app_partitions.py @andrewboie /scripts/dts/ @ulfalizer @galak +/scripts/release/ @nashif /arch/x86/gen_gdt.py @andrewboie /arch/x86/gen_idt.py @andrewboie /scripts/gen_kobject_list.py @andrewboie diff --git a/scripts/release/list_issues.py b/scripts/release/list_issues.py new file mode 100755 index 0000000000..df6aa4257e --- /dev/null +++ b/scripts/release/list_issues.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2019 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +# Lists all closes issues since a given date + +import argparse +import sys +import os +import re +import time +import threading +import requests + + +args = None + + +class Spinner: + busy = False + delay = 0.1 + + @staticmethod + def spinning_cursor(): + while 1: + for cursor in '|/-\\': + yield cursor + + def __init__(self, delay=None): + self.spinner_generator = self.spinning_cursor() + if delay and float(delay): + self.delay = delay + + def spinner_task(self): + while self.busy: + sys.stdout.write(next(self.spinner_generator)) + sys.stdout.flush() + time.sleep(self.delay) + sys.stdout.write('\b') + sys.stdout.flush() + + def __enter__(self): + self.busy = True + threading.Thread(target=self.spinner_task).start() + + def __exit__(self, exception, value, tb): + self.busy = False + time.sleep(self.delay) + if exception is not None: + return False + + +class Issues: + def __init__(self, org, repo, token): + self.repo = repo + self.org = org + self.issues_url = "https://github.com/%s/%s/issues" % ( + self.org, self.repo) + self.github_url = 'https://api.github.com/repos/%s/%s' % ( + self.org, self.repo) + + self.api_token = token + self.headers = {} + self.headers['Authorization'] = 'token %s' % self.api_token + self.headers['Accept'] = 'application/vnd.github.golden-comet-preview+json' + self.items = [] + + def get_pull(self, pull_nr): + url = ("%s/pulls/%s" % (self.github_url, pull_nr)) + response = requests.get("%s" % (url), headers=self.headers) + if response.status_code != 200: + raise RuntimeError( + "Failed to get issue due to unexpected HTTP status code: {}".format( + response.status_code) + ) + item = response.json() + return item + + def get_issue(self, issue_nr): + url = ("%s/issues/%s" % (self.github_url, issue_nr)) + response = requests.get("%s" % (url), headers=self.headers) + if response.status_code != 200: + return None + + item = response.json() + return item + + def list_issues(self, url): + response = requests.get("%s" % (url), headers=self.headers) + if response.status_code != 200: + raise RuntimeError( + "Failed to get issue due to unexpected HTTP status code: {}".format( + response.status_code) + ) + self.items = self.items + response.json() + + try: + print("Getting more items...") + next_issues = response.links["next"] + if next_issues: + next_url = next_issues['url'] + self.list_issues(next_url) + except KeyError: + pass + + def issues_since(self, date, state="closed"): + self.list_issues("%s/issues?state=%s&since=%s" % + (self.github_url, state, date)) + + def pull_requests(self, base='v1.14-branch', state='closed'): + self.list_issues("%s/pulls?state=%s&base=%s" % + (self.github_url, state, base)) + + +def parse_args(): + global args + + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + + parser.add_argument("-o", "--org", default="zephyrproject-rtos", + help="Github organisation") + + parser.add_argument("-r", "--repo", default="zephyr", + help="Github repository") + + parser.add_argument("-f", "--file", required=True, + help="Name of output file.") + + parser.add_argument("-s", "--issues-since", + help="""List issues since date where date + is in the format 2019-09-01.""") + + parser.add_argument("-b", "--issues-in-pulls", + help="List issues in pulls for a given branch") + + parser.add_argument("-c", "--commits-file", + help="""File with all commits (git log a..b) to + be parsed for fixed bugs.""") + + args = parser.parse_args() + + +def main(): + parse_args() + + token = os.environ.get('GH_TOKEN', None) + if not token: + sys.exit("""Github token not set in environment, +set the env. variable GH_TOKEN please and retry.""") + + i = Issues(args.org, args.repo, token) + + if args.issues_since: + i.issues_since(args.issues_since) + count = 0 + with open(args.file, "w") as f: + for issue in i.items: + if 'pull_request' not in issue: + # * :github:`8193` - STM32 config BUILD_OUTPUT_HEX fail + f.write("* :github:`{}` - {}\n".format( + issue['number'], issue['title'])) + count = count + 1 + elif args.issues_in_pulls: + i.pull_requests(base=args.issues_in_pulls) + count = 0 + + bugs = set() + backports = [] + for issue in i.items: + if not isinstance(issue['body'], str): + continue + match = re.findall(r"(Fixes|Closes|Fixed|close):? #([0-9]+)", + issue['body'], re.MULTILINE) + if match: + for mm in match: + bugs.add(mm[1]) + else: + match = re.findall( + r"Backport #([0-9]+)", issue['body'], re.MULTILINE) + if match: + backports.append(match[0]) + + # follow PRs to their origin (backports) + with Spinner(): + for p in backports: + item = i.get_pull(p) + match = re.findall(r"(Fixes|Closes|Fixed|close):? #([0-9]+)", + item['body'], re.MULTILINE) + for mm in match: + bugs.add(mm[1]) + + # now open commits + if args.commits_file: + print("Open commits file and parse for fixed bugs...") + with open(args.commits_file, "r") as commits: + content = commits.read() + match = re.findall(r"(Fixes|Closes|Fixed|close):? #([0-9]+)", + str(content), re.MULTILINE) + for mm in match: + bugs.add(mm[1]) + + print("Create output file...") + with Spinner(): + with open(args.file, "w") as f: + for m in sorted(bugs): + item = i.get_issue(m) + if item: + # * :github:`8193` - STM32 config BUILD_OUTPUT_HEX fail + f.write("* :github:`{}` - {}\n".format( + item['number'], item['title'])) + +if __name__ == '__main__': + main()