2017-08-03 09:21:20 +02:00
|
|
|
#!/usr/bin/env python
|
|
|
|
# -*- coding:utf-8 -*-
|
|
|
|
|
|
|
|
import requests
|
|
|
|
import re
|
|
|
|
import pickle
|
|
|
|
import sys
|
|
|
|
import os
|
|
|
|
from datetime import datetime
|
|
|
|
from collections import defaultdict
|
2018-01-16 15:49:53 +01:00
|
|
|
from collections import OrderedDict
|
|
|
|
|
|
|
|
#################################
|
|
|
|
## Env Config
|
2017-08-03 09:21:20 +02:00
|
|
|
|
|
|
|
try:
|
|
|
|
github_auth_username = os.environ['ICINGA_GITHUB_AUTH_USERNAME']
|
|
|
|
except KeyError:
|
|
|
|
print "ERROR: Environment variable 'ICINGA_GITHUB_AUTH_USERNAME' is not set."
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
try:
|
|
|
|
github_auth_token = os.environ['ICINGA_GITHUB_AUTH_TOKEN']
|
|
|
|
except:
|
|
|
|
print "ERROR: Environment variable 'ICINGA_GITHUB_AUTH_TOKEN' is not set."
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
try:
|
|
|
|
project_name = os.environ['ICINGA_GITHUB_PROJECT']
|
|
|
|
except:
|
|
|
|
print "ERROR: Environment variable 'ICINGA_GITHUB_PROJECT' is not set."
|
|
|
|
sys.exit(1)
|
|
|
|
|
2018-01-16 15:49:53 +01:00
|
|
|
#################################
|
|
|
|
## Config
|
|
|
|
|
2017-08-03 09:21:20 +02:00
|
|
|
changelog_file = "CHANGELOG.md" # TODO: config param
|
|
|
|
debug = 1
|
|
|
|
|
2018-01-16 15:49:53 +01:00
|
|
|
# Keep this in sync with GitHub labels.
|
|
|
|
ignored_labels = [
|
|
|
|
"high-priority", "low-priority",
|
|
|
|
"bug", "enhancement",
|
|
|
|
"needs-feedback", "question", "duplicate", "invalid", "wontfix",
|
|
|
|
"backported", "build-fix"
|
|
|
|
]
|
|
|
|
|
|
|
|
# Selectively show and collect specific categories
|
|
|
|
#
|
|
|
|
# (category, list of case sensitive matching labels)
|
|
|
|
# The order is important!
|
|
|
|
# Keep this in sync with GitHub labels.
|
|
|
|
categories = OrderedDict(
|
|
|
|
[
|
|
|
|
("Enhancement", ["enhancement"]),
|
|
|
|
("Bug", ["bug", "crash"]),
|
|
|
|
("ITL", ["ITL"]),
|
|
|
|
("Documentation", ["Documentation"]),
|
|
|
|
("Support", ["code-quality", "Tests", "Packages", "Installation"])
|
|
|
|
]
|
|
|
|
)
|
2017-09-21 10:45:48 +02:00
|
|
|
|
2017-08-03 09:21:20 +02:00
|
|
|
#################################
|
|
|
|
## Helpers
|
|
|
|
|
|
|
|
def write_changelog(line):
|
|
|
|
clfp.write(line + "\n")
|
|
|
|
|
|
|
|
def log(level, msg):
|
|
|
|
if level <= debug:
|
|
|
|
print " " + msg
|
|
|
|
|
|
|
|
def fetch_github_resources(uri, params = {}):
|
|
|
|
resources = []
|
|
|
|
|
|
|
|
url = 'https://api.github.com/repos/' + project_name + uri + "?per_page=100" # 100 is the maximum
|
|
|
|
|
|
|
|
while True:
|
|
|
|
log(2, "Requesting URL: " + url)
|
|
|
|
resp = requests.get(url, auth=(github_auth_username, github_auth_token), params=params)
|
|
|
|
try:
|
|
|
|
resp.raise_for_status()
|
2017-09-25 16:05:05 +02:00
|
|
|
except requests.exceptions.HTTPError as e:
|
|
|
|
raise e
|
2017-08-03 09:21:20 +02:00
|
|
|
|
|
|
|
data = resp.json()
|
|
|
|
|
|
|
|
if len(data) == 0:
|
|
|
|
break
|
|
|
|
|
|
|
|
resources.extend(data)
|
|
|
|
|
|
|
|
# fetch the next page from headers, do not count pages
|
|
|
|
# http://engineering.hackerearth.com/2014/08/21/python-requests-module/
|
|
|
|
if "next" in resp.links:
|
|
|
|
url = resp.links['next']['url']
|
|
|
|
log(2, "Found next link for Github pagination: " + url)
|
|
|
|
else:
|
|
|
|
break # no link found, we are done
|
|
|
|
log(2, "No more pages to fetch, stop.")
|
|
|
|
|
|
|
|
return resources
|
|
|
|
|
|
|
|
def issue_type(issue):
|
2018-01-16 15:49:53 +01:00
|
|
|
issue_labels = [label["name"] for label in issue["labels"]]
|
|
|
|
|
|
|
|
# start with the least important first (e.g. "Support", "Documentation", "Bug", "Enhancement" as order)
|
|
|
|
for category in reversed(categories):
|
|
|
|
labels = categories[category]
|
|
|
|
|
|
|
|
for label in labels:
|
|
|
|
if label in issue_labels:
|
|
|
|
return category
|
|
|
|
|
|
|
|
return "Support"
|
2017-08-03 09:21:20 +02:00
|
|
|
|
|
|
|
def escape_markdown(text):
|
|
|
|
#tmp = text.replace('&', '&').replace('<', '<').replace('>', '>')
|
|
|
|
tmp = text
|
|
|
|
tmp.replace('\\', '\\\\')
|
|
|
|
|
|
|
|
return re.sub("([<>*_()\[\]#])", r"\\\1", tmp)
|
|
|
|
|
|
|
|
def format_labels(issue):
|
2017-09-21 10:45:48 +02:00
|
|
|
labels = filter(lambda label: label not in ignored_labels, [label["name"] for label in issue["labels"]])
|
2017-08-03 09:21:20 +02:00
|
|
|
|
2018-01-16 15:49:53 +01:00
|
|
|
# Mark PRs as custom label
|
|
|
|
if "pull_request" in issue:
|
|
|
|
labels.append("PR")
|
|
|
|
|
2017-08-03 09:21:20 +02:00
|
|
|
if len(labels):
|
|
|
|
return " (" + ", ".join(labels) + ")"
|
|
|
|
else:
|
|
|
|
return ""
|
|
|
|
|
|
|
|
def format_title(title):
|
|
|
|
# Fix encoding
|
|
|
|
try:
|
|
|
|
issue_title = str(title.encode('ascii', 'ignore').encode('utf-8'))
|
|
|
|
except Error:
|
|
|
|
log(1, "Error: Cannot convert " + title + " to UTF-8")
|
|
|
|
|
|
|
|
# Remove dev.icinga.com tag
|
|
|
|
issue_title = re.sub('\[dev\.icinga\.com #\d+\] ', '', issue_title)
|
|
|
|
|
|
|
|
#log(1, "Issue title: " + issue_title + "Type: " + str(type(issue_title)))
|
|
|
|
|
|
|
|
return escape_markdown(issue_title)
|
|
|
|
|
|
|
|
#################################
|
|
|
|
## MAIN
|
|
|
|
|
|
|
|
milestones = {}
|
|
|
|
issues = defaultdict(lambda: defaultdict(list))
|
2017-09-25 16:05:05 +02:00
|
|
|
|
|
|
|
log(1, "Fetching data from GitHub API for project " + project_name)
|
2017-08-03 09:21:20 +02:00
|
|
|
|
2017-09-25 16:05:05 +02:00
|
|
|
try:
|
|
|
|
tickets = fetch_github_resources("/issues", { "state": "all" })
|
|
|
|
except requests.exceptions.HTTPError as e:
|
|
|
|
log(1, "ERROR " + str(e.response.status_code) + ": " + e.response.text)
|
|
|
|
|
|
|
|
sys.exit(1)
|
2017-08-03 09:21:20 +02:00
|
|
|
|
|
|
|
clfp = open(changelog_file, "w+")
|
|
|
|
|
|
|
|
with open('tickets.pickle', 'wb') as fp:
|
2017-09-25 16:05:05 +02:00
|
|
|
pickle.dump(tickets, fp)
|
2017-08-03 09:21:20 +02:00
|
|
|
|
|
|
|
with open('tickets.pickle', 'rb') as fp:
|
|
|
|
cached_issues = pickle.load(fp)
|
|
|
|
|
|
|
|
for issue in cached_issues: #fetch_github_resources("/issues", { "state": "all" }):
|
|
|
|
milestone = issue["milestone"]
|
|
|
|
|
|
|
|
if not milestone:
|
|
|
|
continue
|
|
|
|
|
|
|
|
ms_title = milestone["title"]
|
|
|
|
|
|
|
|
if not re.match('^\d+\.\d+\.\d+$', ms_title):
|
|
|
|
continue
|
|
|
|
|
|
|
|
if ms_title.split(".")[0] != "2":
|
|
|
|
continue
|
|
|
|
|
|
|
|
milestones[ms_title] = milestone
|
|
|
|
|
|
|
|
ms_tickets = issues[ms_title][issue_type(issue)]
|
|
|
|
ms_tickets.append(issue)
|
|
|
|
|
2017-09-28 15:41:43 +02:00
|
|
|
# TODO: Generic header based on project_name
|
2017-08-03 09:21:20 +02:00
|
|
|
write_changelog("# Icinga 2.x CHANGELOG")
|
|
|
|
write_changelog("")
|
|
|
|
|
|
|
|
for milestone in sorted(milestones.values(), key=lambda ms: (ms["due_on"], ms["title"]), reverse=True):
|
|
|
|
if milestone["state"] != "closed":
|
|
|
|
continue
|
|
|
|
|
|
|
|
if milestone["due_on"] == None:
|
2019-02-25 16:53:13 +01:00
|
|
|
print "Warning: Milestone", milestone["title"], "does not have a due date."
|
2017-08-03 09:21:20 +02:00
|
|
|
|
|
|
|
ms_due_on = datetime.strptime(milestone["due_on"], "%Y-%m-%dT%H:%M:%SZ")
|
|
|
|
|
|
|
|
write_changelog("## %s (%s)" % (milestone["title"], ms_due_on.strftime("%Y-%m-%d")))
|
|
|
|
write_changelog("")
|
|
|
|
|
|
|
|
ms_description = milestone["description"]
|
|
|
|
ms_description = re.sub('\r\n', '\n', ms_description)
|
|
|
|
|
|
|
|
if len(ms_description) > 0:
|
|
|
|
write_changelog("### Notes\n\n" + ms_description + "\n") # Don't escape anything, we take care on Github for valid Markdown
|
|
|
|
|
2018-01-16 15:49:53 +01:00
|
|
|
for category, labels in categories.iteritems():
|
2017-08-03 09:21:20 +02:00
|
|
|
try:
|
|
|
|
ms_issues = issues[milestone["title"]][category]
|
|
|
|
except KeyError:
|
|
|
|
continue
|
|
|
|
|
|
|
|
if len(ms_issues) == 0:
|
|
|
|
continue
|
|
|
|
|
|
|
|
write_changelog("### " + category)
|
|
|
|
write_changelog("")
|
|
|
|
|
|
|
|
for issue in ms_issues:
|
|
|
|
write_changelog("* [#" + str(issue["number"]) + "](https://github.com/" + project_name
|
|
|
|
+ "/issues/" + str(issue["number"]) + ")" + format_labels(issue) + ": " + format_title(issue["title"]))
|
|
|
|
|
|
|
|
write_changelog("")
|
|
|
|
|
|
|
|
clfp.close()
|
|
|
|
log(1, "Finished writing " + changelog_file)
|