mirror of
https://github.com/Icinga/icinga2.git
synced 2025-04-08 17:05:25 +02:00
This PR allows to adjust the categories and matching labels. If no label matches, "Support" is the old hardcoded default. Issues with the "Documentation" and "ITL" label will be put into their respective category at the bottom before "Support". This increases readability and allows users to focus on the core vs additional config and docs. "Support" also applies to labels such as "code-quality", "Tests" and "Packages"/"Installation". Note: Labels are case sensitive. The order of the categories dictionary is important too. Since issues and PRs are the same for GitHub, and we don't require an issue for a PR anymore, we sometimes have duplicates. This patch adds an inline label called "PR" to highlight these PRs in the Changelog.
235 lines
6.4 KiB
Python
Executable File
235 lines
6.4 KiB
Python
Executable File
#!/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
|
|
from collections import OrderedDict
|
|
|
|
#################################
|
|
## Env Config
|
|
|
|
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)
|
|
|
|
#################################
|
|
## Config
|
|
|
|
changelog_file = "CHANGELOG.md" # TODO: config param
|
|
debug = 1
|
|
|
|
# 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"])
|
|
]
|
|
)
|
|
|
|
#################################
|
|
## 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()
|
|
except requests.exceptions.HTTPError as e:
|
|
raise e
|
|
|
|
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):
|
|
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"
|
|
|
|
def escape_markdown(text):
|
|
#tmp = text.replace('&', '&').replace('<', '<').replace('>', '>')
|
|
tmp = text
|
|
tmp.replace('\\', '\\\\')
|
|
|
|
return re.sub("([<>*_()\[\]#])", r"\\\1", tmp)
|
|
|
|
def format_labels(issue):
|
|
labels = filter(lambda label: label not in ignored_labels, [label["name"] for label in issue["labels"]])
|
|
|
|
# Mark PRs as custom label
|
|
if "pull_request" in issue:
|
|
labels.append("PR")
|
|
|
|
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))
|
|
|
|
log(1, "Fetching data from GitHub API for project " + project_name)
|
|
|
|
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)
|
|
|
|
clfp = open(changelog_file, "w+")
|
|
|
|
with open('tickets.pickle', 'wb') as fp:
|
|
pickle.dump(tickets, fp)
|
|
|
|
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)
|
|
|
|
# TODO: Generic header based on project_name
|
|
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:
|
|
print "Milestone", milestone["title"], "does not have a due date."
|
|
sys.exit(1)
|
|
|
|
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
|
|
|
|
for category, labels in categories.iteritems():
|
|
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)
|