From e1e0b31d10d55e064a699e313adc2afc0f6eb2fd Mon Sep 17 00:00:00 2001 From: Gustav Tiger Date: Tue, 19 Jul 2022 10:18:00 +0200 Subject: [PATCH] ci: update release scripts --- sbin/changelog.py | 64 +++++++++++++++++++++++++++++----------------- sbin/commitlint.py | 4 +-- sbin/config.py | 7 +++++ sbin/utils.py | 58 ++++++++++++++++++++++++++++++++++++++--- 4 files changed, 103 insertions(+), 30 deletions(-) create mode 100644 sbin/config.py diff --git a/sbin/changelog.py b/sbin/changelog.py index c47f4af2..90e0d33b 100755 --- a/sbin/changelog.py +++ b/sbin/changelog.py @@ -6,6 +6,7 @@ import sys import utils +import config # # Stop immediately if version too old. @@ -16,17 +17,10 @@ f"You need Python >= {_MINIMUM_PYTHON_VERSION}, but you are running {sys.version}" ) -GITHUB_COMPARE_URL = ( - "https://github.com/AxisCommunications/practical-react-components/compare" -) -GITHUB_COMMIT_URL = ( - "https://github.com/AxisCommunications/practical-react-components/commit" -) - GROUP_TITLES = { "build": "👷 Build", "chore": "🚧 Maintenance", - "ci": "🚦 Continous integration", + "ci": "🚦 Continuous integration", "docs": "📝 Documentation", "feat": "✨ Features", "fix": "🐛 Bug fixes", @@ -41,14 +35,22 @@ def changelog_part(commitish_to: str, commitish_from: str, version: str): date = utils.cmd(["git", "log", "-1", "--format=%ci", commitish_to]) - commit_range = ( - f"{commitish_from}..HEAD" - if commitish_to == "HEAD" - else f"{commitish_from}..{commitish_to}~" - ) + if commitish_from is None: + commit_range = commitish_to + elif commitish_to == "HEAD": + commit_range = f"{commitish_from}..HEAD" + else: + commit_range = f"{commitish_from}..{commitish_to}~" commits = utils.cmd( - ["git", "log", "--no-merges", "--date-order", "--format=%H%x09%s", commit_range] + [ + "git", + "log", + "--no-merges", + "--date-order", + "--format=%H%n%h%n%B%x1f", + commit_range, + ] ) if commits == "": @@ -56,21 +58,22 @@ def changelog_part(commitish_to: str, commitish_from: str, version: str): messages = {} - for commit in commits.split("\n"): - sha, msg = commit.split(maxsplit=1) - shortsha = utils.cmd(["git", "log", "-1", "--format=%h", sha]) + for commit in commits.split("\x1f"): + sha, shortsha, msg = commit.strip().split("\n", maxsplit=2) try: data = utils.conventional_commit_parse(msg) + issues = utils.closing_issues_commit_parse(msg) messages.setdefault(data["type"], []).append( - {**data, "sha": sha, "shortsha": shortsha} + {**data, "issues": issues, "sha": sha, "shortsha": shortsha} ) except: # No conventional commit pass content = [ - f"## [{version}]({GITHUB_COMPARE_URL}/{commitish_from}...{version}) ({date})" + f"## [{version}]({config.GITHUB_RELEASE_URL}/{version})", + f"{date}, [Compare changes]({config.GITHUB_COMPARE_URL}/{commitish_from}...{version})", ] for group in GROUP_TITLES.keys(): @@ -81,10 +84,24 @@ def changelog_part(commitish_to: str, commitish_from: str, version: str): for data in messages[group]: - prefix = ( - f' - **{data["scope"]}**: ' if data["scope"] is not None else " - " + prefix = " - " + + pull_request = utils.get_github_pull_request(data["sha"]) + # pull_request[0] is id, pull_request[1] is url + if pull_request is not None: + prefix += f"[!{pull_request[0]}]({pull_request[1]}) - " + + if data["scope"] is not None: + prefix += f'**{data["scope"]}**: ' + + postfix = ( + f' ([`{data["shortsha"]}`]({config.GITHUB_COMMIT_URL}/{data["sha"]}))' ) - postfix = f' ([{data["shortsha"]}]({GITHUB_COMMIT_URL}/{data["sha"]}))' + + commit_author = utils.get_github_author(data["sha"]) + # commit_author[0] is username, commit_author[1] is url + if commit_author is not None: + postfix += f" ([**@{commit_author[0]}**]({commit_author[1]}))" if data["breaking"]: content.append(f'{prefix}**BREAKING** {data["description"]}{postfix}') @@ -96,9 +113,7 @@ def changelog_part(commitish_to: str, commitish_from: str, version: str): HEADER = """ # Changelog - All notable changes to this project will be documented in this file. - """ @@ -162,6 +177,7 @@ def changelog_part(commitish_to: str, commitish_from: str, version: str): if args.type == "full" and args.release is not None: tags.insert(0, "HEAD") + tags.append(None) content = [HEADER] if not args.skip_header else [] diff --git a/sbin/commitlint.py b/sbin/commitlint.py index 5747d7b5..9a2cc8e9 100755 --- a/sbin/commitlint.py +++ b/sbin/commitlint.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import argparse import sys @@ -11,11 +11,9 @@ description=""" If no range is given, HEAD~..HEAD is used (so only the latest commit will be checked). - Note that the a range fa56eb..HEAD does not include the fa56eb commit (to start from e.g. fa56eb, you would write fa56eb~..HEAD to use the parent as starting point). - Check if message conforms to a conventional commit message, see https://www.conventionalcommits.org/en/v1.0.0/#specification """ diff --git a/sbin/config.py b/sbin/config.py new file mode 100644 index 00000000..0ffc136e --- /dev/null +++ b/sbin/config.py @@ -0,0 +1,7 @@ +GITHUB_URL = "https://github.com" +GITHUB_API_URL = "https://api.github.com" +GITHUB_REPOSITORY = "AxisCommunications/practical-react-components" + +GITHUB_COMPARE_URL = f"{GITHUB_URL}/{GITHUB_REPOSITORY}/compare" +GITHUB_COMMIT_URL = f"{GITHUB_URL}/{GITHUB_REPOSITORY}/commit" +GITHUB_RELEASE_URL = f"{GITHUB_URL}/{GITHUB_REPOSITORY}/releases/tag" diff --git a/sbin/utils.py b/sbin/utils.py index db46331d..7b778c60 100644 --- a/sbin/utils.py +++ b/sbin/utils.py @@ -1,8 +1,13 @@ +import os +import urllib.request +import json import re import subprocess import sys import typing +import config + possible_types = [ "build", "chore", @@ -19,14 +24,16 @@ types = "|".join(possible_types) -re_conventional_commit_header = re.compile(fr"^({types})(?:\(([^\)]+)\))?(!?): (.*)$") +re_conventional_commit_header = re.compile( + rf"^({types})(?:\(([^\)]+)\))?(!?): (.*)(?:\n|$)" +) def conventional_commit_parse(message: str): - match = re.fullmatch(re_conventional_commit_header, message) + match = re.match(re_conventional_commit_header, message) if match is None: - raise Exception() + raise Exception("Not a conventional commit") type, scope, breaking, header = match.groups() @@ -38,6 +45,51 @@ def conventional_commit_parse(message: str): } +re_closing_issues = re.compile( + r"^Closes: ([A-Z]+-[0-9]+)$", re.MULTILINE | re.IGNORECASE +) + + +def closing_issues_commit_parse(message: str): + return re.findall(re_closing_issues, message) + + +def get_github_api(url: str): + try: + token = os.environ["GITHUB_TOKEN"] + req = urllib.request.Request( + f"{config.GITHUB_API_URL}{url}", + headers={"Authorization": f"Bearer {token}"}, + ) + res = urllib.request.urlopen(req) + data = json.load(res) + + return data + except KeyError as e: + print("GITHUB_TOKEN environment not set") + sys.exit(1) + except urllib.error.HTTPError as e: + return None + + +def get_github_pull_request(sha: str): + data = get_github_api(f"/repos/{config.GITHUB_REPOSITORY}/commits/{sha}/pulls") + + if data is None or len(data) == 0: + return None + + return (data[0]["number"], data[0]["html_url"]) + + +def get_github_author(sha: str): + data = get_github_api(f"/repos/{config.GITHUB_REPOSITORY}/commits/{sha}") + + if data is None or data["author"] is None: + return None + + return (data["author"]["login"], data["author"]["html_url"]) + + def cmd(cmd: typing.List[str]) -> str: """Call shell command and return the result""" try: