Skip to content
This repository was archived by the owner on Mar 25, 2025. It is now read-only.

Commit 2feba5f

Browse files
committed
chore: upgrade release scripts
1 parent 3bc4288 commit 2feba5f

File tree

6 files changed

+500
-321
lines changed

6 files changed

+500
-321
lines changed

CHANGELOG.md

Lines changed: 368 additions & 268 deletions
Large diffs are not rendered by default.

sbin/changelog.py

Lines changed: 55 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
#!/usr/bin/env python3
22

33
import argparse
4-
import re
5-
import subprocess
64
import sys
75

86
import utils
7+
import config
98

109
#
1110
# Stop immediately if version too old.
@@ -16,17 +15,10 @@
1615
f"You need Python >= {_MINIMUM_PYTHON_VERSION}, but you are running {sys.version}"
1716
)
1817

19-
GITHUB_COMPARE_URL = (
20-
"https://github.com/AxisCommunications/practical-react-components/compare"
21-
)
22-
GITHUB_COMMIT_URL = (
23-
"https://github.com/AxisCommunications/practical-react-components/commit"
24-
)
25-
2618
GROUP_TITLES = {
2719
"build": "👷 Build",
2820
"chore": "🚧 Maintenance",
29-
"ci": "🚦 Continous integration",
21+
"ci": "🚦 Continuous integration",
3022
"docs": "📝 Documentation",
3123
"feat": "✨ Features",
3224
"fix": "🐛 Bug fixes",
@@ -41,36 +33,45 @@
4133
def changelog_part(commitish_to: str, commitish_from: str, version: str):
4234
date = utils.cmd(["git", "log", "-1", "--format=%ci", commitish_to])
4335

44-
commit_range = (
45-
f"{commitish_from}..HEAD"
46-
if commitish_to == "HEAD"
47-
else f"{commitish_from}..{commitish_to}~"
48-
)
36+
if commitish_from is None:
37+
commit_range = commitish_to
38+
elif commitish_to == "HEAD":
39+
commit_range = f"{commitish_from}..HEAD"
40+
else:
41+
commit_range = f"{commitish_from}..{commitish_to}~"
4942

5043
commits = utils.cmd(
51-
["git", "log", "--no-merges", "--date-order", "--format=%H%x09%s", commit_range]
44+
[
45+
"git",
46+
"log",
47+
"--no-merges",
48+
"--date-order",
49+
"--format=%H%n%h%n%B%x1f",
50+
commit_range,
51+
]
5252
)
5353

5454
if commits == "":
5555
return ""
5656

5757
messages = {}
5858

59-
for commit in commits.split("\n"):
60-
sha, msg = commit.split(maxsplit=1)
61-
shortsha = utils.cmd(["git", "log", "-1", "--format=%h", sha])
59+
for commit in commits.split("\x1f"):
60+
sha, shortsha, msg = commit.strip().split("\n", maxsplit=2)
6261

6362
try:
6463
data = utils.conventional_commit_parse(msg)
64+
issues = utils.closing_issues_commit_parse(msg)
6565
messages.setdefault(data["type"], []).append(
66-
{**data, "sha": sha, "shortsha": shortsha}
66+
{**data, "issues": issues, "sha": sha, "shortsha": shortsha}
6767
)
6868
except:
6969
# No conventional commit
7070
pass
7171

7272
content = [
73-
f"## [{version}]({GITHUB_COMPARE_URL}/{commitish_from}...{version}) ({date})"
73+
f"## [{version}]({config.GITHUB_RELEASE_URL}/{version})",
74+
f"{date}, [Compare changes]({config.GITHUB_COMPARE_URL}/{commitish_from}...{version})",
7475
]
7576

7677
for group in GROUP_TITLES.keys():
@@ -81,10 +82,24 @@ def changelog_part(commitish_to: str, commitish_from: str, version: str):
8182

8283
for data in messages[group]:
8384

84-
prefix = (
85-
f' - **{data["scope"]}**: ' if data["scope"] is not None else " - "
85+
prefix = " - "
86+
87+
pull_request = utils.get_github_pull_request(data["sha"])
88+
# pull_request[0] is id, pull_request[1] is url
89+
if pull_request is not None:
90+
prefix += f"[!{pull_request[0]}]({pull_request[1]}) - "
91+
92+
if data["scope"] is not None:
93+
prefix += f'**{data["scope"]}**: '
94+
95+
postfix = (
96+
f' ([`{data["shortsha"]}`]({config.GITHUB_COMMIT_URL}/{data["sha"]}))'
8697
)
87-
postfix = f' ([{data["shortsha"]}]({GITHUB_COMMIT_URL}/{data["sha"]}))'
98+
99+
commit_author = utils.get_github_author(data["sha"])
100+
# commit_author[0] is username, commit_author[1] is url
101+
if commit_author is not None:
102+
postfix += f" ([**@{commit_author[0]}**]({commit_author[1]}))"
88103

89104
if data["breaking"]:
90105
content.append(f'{prefix}**BREAKING** {data["description"]}{postfix}')
@@ -96,7 +111,6 @@ def changelog_part(commitish_to: str, commitish_from: str, version: str):
96111

97112
HEADER = """
98113
# Changelog
99-
100114
All notable changes to this project will be documented in this file.
101115
102116
"""
@@ -114,6 +128,14 @@ def changelog_part(commitish_to: str, commitish_from: str, version: str):
114128
help="Don't include a changelog header",
115129
)
116130

131+
parser.add_argument(
132+
"-r",
133+
"--release",
134+
type=str,
135+
metavar="RELEASE",
136+
help="New release tag (e.g. vX.Y.Z), includes full changelog with a new entry for things not tagged",
137+
)
138+
117139
subparsers = parser.add_subparsers(
118140
dest="type", metavar="COMMAND", help='One of "single" or "full".', required=True
119141
)
@@ -126,13 +148,6 @@ def changelog_part(commitish_to: str, commitish_from: str, version: str):
126148
full = subparsers.add_parser(
127149
"full", description="Generate a changelog covering entire history."
128150
)
129-
full.add_argument(
130-
"-r",
131-
"--release",
132-
type=str,
133-
metavar="RELEASE",
134-
help="New release tag (e.g. vX.Y.Z), includes full changelog with a new entry for things not tagged",
135-
)
136151

137152
args = parser.parse_args()
138153

@@ -149,6 +164,10 @@ def changelog_part(commitish_to: str, commitish_from: str, version: str):
149164
]
150165
).split()
151166

167+
if args.release is not None:
168+
tags.insert(0, "HEAD")
169+
tags.append(None)
170+
152171
if args.type == "single":
153172
if args.tag is None:
154173
try:
@@ -160,23 +179,19 @@ def changelog_part(commitish_to: str, commitish_from: str, version: str):
160179
print(f"Error: tag {args.tag} not found!")
161180
sys.exit(1)
162181

163-
if args.type == "full" and args.release is not None:
164-
tags.insert(0, "HEAD")
165-
166-
content = [HEADER] if not args.skip_header else []
182+
if not args.skip_header:
183+
sys.stdout.write(HEADER)
167184

168185
for commitish_to, commitish_from in zip(tags[:-1], tags[1:]):
169186
if args.type == "single" and args.tag != commitish_to:
170187
continue
171-
172-
content.append(
188+
sys.stdout.write(
173189
changelog_part(
174190
commitish_to,
175191
commitish_from,
176192
args.release if commitish_to == "HEAD" else commitish_to,
177193
)
178194
)
179-
content.append("")
195+
sys.stdout.write("\n\n")
180196

181-
sys.stdout.write("\n".join(content))
182197
sys.stdout.close()

sbin/commitlint.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
#!/usr/bin/env python
1+
#!/usr/bin/env python3
22

33
import argparse
44
import sys
5-
import subprocess
65

76
import utils
87

@@ -11,11 +10,9 @@
1110
description="""
1211
If no range is given, HEAD~..HEAD is used (so only the latest commit
1312
will be checked).
14-
1513
Note that the a range fa56eb..HEAD does not include the fa56eb commit
1614
(to start from e.g. fa56eb, you would write fa56eb~..HEAD to use the parent
1715
as starting point).
18-
1916
Check if message conforms to a conventional commit message, see
2017
https://www.conventionalcommits.org/en/v1.0.0/#specification
2118
"""

sbin/config.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
GITHUB_URL = "https://github.com"
2+
GITHUB_API_URL = "https://api.github.com"
3+
GITHUB_REPOSITORY = "AxisCommunications/practical-react-components"
4+
5+
GITHUB_COMPARE_URL = f"{GITHUB_URL}/{GITHUB_REPOSITORY}/compare"
6+
GITHUB_COMMIT_URL = f"{GITHUB_URL}/{GITHUB_REPOSITORY}/commit"
7+
GITHUB_RELEASE_URL = f"{GITHUB_URL}/{GITHUB_REPOSITORY}/releases/tag"

sbin/release.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import json
55

66
import utils
7+
import changelog
78

89
parser = argparse.ArgumentParser()
910
parser.add_argument(
@@ -34,7 +35,7 @@
3435
"--increment",
3536
args.level,
3637
"--preid",
37-
"alpha",
38+
"beta",
3839
]
3940
)
4041
next_tag = f"v{next_version}"
@@ -45,11 +46,17 @@
4546
utils.cmd(["yarn", "version", "apply", "--all"])
4647

4748
print(" - Update changelog")
48-
changelog = utils.cmd(["./sbin/changelog.py", "full", "--release", next_tag])
49-
with open("CHANGELOG.md", "w") as f:
50-
f.write(changelog)
49+
changelog_part = utils.cmd(
50+
["./sbin/changelog.py", "--release", next_tag, "--skip-header", "single"]
51+
)
52+
with open("CHANGELOG.md", "r+") as f:
53+
old = f.read()
54+
f.seek(0)
55+
f.write(changelog.HEADER)
56+
f.write(changelog_part)
57+
f.write("\n\n")
58+
f.write(old[len(changelog.HEADER) :])
5159

5260
print(" - Create release commit and tag")
53-
utils.cmd(["git", "add", "-u"])
54-
utils.cmd(["git", "commit", "-m", next_tag])
61+
utils.cmd(["git", "commit", "-a", "-m", next_tag])
5562
utils.cmd(["git", "tag", "-a", "-m", next_tag, next_tag])

sbin/utils.py

100644100755
Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1+
import os
2+
import urllib.request
3+
import json
14
import re
25
import subprocess
36
import sys
47
import typing
58

9+
import config
10+
611
possible_types = [
712
"build",
813
"chore",
@@ -19,14 +24,16 @@
1924

2025
types = "|".join(possible_types)
2126

22-
re_conventional_commit_header = re.compile(fr"^({types})(?:\(([^\)]+)\))?(!?): (.*)$")
27+
re_conventional_commit_header = re.compile(
28+
rf"^({types})(?:\(([^\)]+)\))?(!?): (.*)(?:\n|$)"
29+
)
2330

2431

2532
def conventional_commit_parse(message: str):
26-
match = re.fullmatch(re_conventional_commit_header, message)
33+
match = re.match(re_conventional_commit_header, message)
2734

2835
if match is None:
29-
raise Exception()
36+
raise Exception("Not a conventional commit")
3037

3138
type, scope, breaking, header = match.groups()
3239

@@ -38,6 +45,52 @@ def conventional_commit_parse(message: str):
3845
}
3946

4047

48+
re_closing_issues = re.compile(
49+
r"^Closes: ([A-Z]+-[0-9]+)$", re.MULTILINE | re.IGNORECASE
50+
)
51+
52+
53+
def closing_issues_commit_parse(message: str):
54+
return re.findall(re_closing_issues, message)
55+
56+
57+
def get_github_api(url: str):
58+
try:
59+
token = os.environ["GITHUB_TOKEN"]
60+
req = urllib.request.Request(
61+
f"{config.GITHUB_API_URL}{url}",
62+
headers={"Authorization": f"Bearer {token}"},
63+
)
64+
res = urllib.request.urlopen(req)
65+
data = json.load(res)
66+
67+
return data
68+
except KeyError as e:
69+
print("GITHUB_TOKEN environment not set")
70+
sys.exit(1)
71+
except urllib.error.HTTPError as e:
72+
print("GitHub API request failed:", e)
73+
sys.exit(1)
74+
75+
76+
def get_github_pull_request(sha: str):
77+
data = get_github_api(f"/repos/{config.GITHUB_REPOSITORY}/commits/{sha}/pulls")
78+
79+
if data is None or len(data) == 0:
80+
return None
81+
82+
return (data[0]["number"], data[0]["html_url"])
83+
84+
85+
def get_github_author(sha: str):
86+
data = get_github_api(f"/repos/{config.GITHUB_REPOSITORY}/commits/{sha}")
87+
88+
if data is None or data["author"] is None:
89+
return None
90+
91+
return (data["author"]["login"], data["author"]["html_url"])
92+
93+
4194
def cmd(cmd: typing.List[str]) -> str:
4295
"""Call shell command and return the result"""
4396
try:

0 commit comments

Comments
 (0)