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 <anas.nashif@intel.com>
diff --git a/CODEOWNERS b/CODEOWNERS
index ff75be8..99a3411 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 0000000..df6aa42
--- /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()