Skip to content

Commit 5302d0e

Browse files
committed
tools: add a rudimentary make-a-release script
To make mistake less likely. This is very much untested and subject to change. We will find out how well this works in next picom release. Signed-off-by: Yuxuan Shui <[email protected]>
1 parent d04840f commit 5302d0e

File tree

4 files changed

+180
-1
lines changed

4 files changed

+180
-1
lines changed

.github/pull_request_template.md

+6
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
11
<!-- Please enable "Allow edits from maintainers" so we can make necessary changes to your PR -->
2+
3+
# Changelog
4+
5+
<!-- If you want to add a changelog entry for this PR, write it here; otherwise delete this section -->
6+
* Category: (Bug fixes, new features, etc.)
7+
* Description: (Describe changes made)

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ target
4848
# Python
4949
*.pyc
5050
*.py
51+
!/tools/*.py
5152
!/tests/testcases/*.py
5253

5354
# Misc files

package.nix

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ stdenv.mkDerivation (finalAttrs: {
6161
llvmPackages_18.clang-unwrapped.python
6262
llvmPackages_18.libllvm
6363
(python3.withPackages (ps: with ps; [
64-
xcffib pip dbus-next
64+
xcffib pip dbus-next pygit2
6565
]))
6666
]);
6767

tools/make-a-release.py

+172
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
# usage: ./make-a-release.py [previous_version]
2+
# create a new release tag and update CHANGELOG.md. If previous-version is not
3+
# provided, it will be inferred from the latest git tag.
4+
5+
# XXX: I set out to use pygit2, but turns out certain things I need aren't available there,
6+
# so this script uses git command line also. ¯\_(ツ)_/¯
7+
import pygit2
8+
import datetime
9+
import sys
10+
import tempfile
11+
import subprocess
12+
import re
13+
from pathlib import Path
14+
15+
repo = pygit2.Repository('.')
16+
17+
status = repo.status(untracked_files='no')
18+
for filepath, flags in status.items():
19+
if flags != pygit2.enums.FileStatus.CURRENT:
20+
print(f"Filepath {filepath} isn't clean")
21+
print("Repository is dirty, please commit or stash your changes")
22+
sys.exit(1)
23+
24+
all_tags = {tag.peel(pygit2.Commit).id: tag.shorthand for tag in repo.references.iterator(pygit2.enums.ReferenceFilter.TAGS)}
25+
previous_version = sys.argv[1] if len(sys.argv) > 1 else None
26+
merge_base = None
27+
28+
# now, since stable releases are branched off from next, and some commits in next might have already been backported/cherry-picked to stable,
29+
# so we need to go through unique commits in the previous stable release, and filter out the ones that were already released.
30+
31+
if previous_version is None:
32+
queue = [repo.head.peel(pygit2.Commit)]
33+
h = 0
34+
while h < len(queue) and queue[h].id not in all_tags:
35+
queue.extend(queue[h].parents)
36+
h += 1
37+
print(f'Previous release is {all_tags[queue[h].id]}, commit {queue[h].id}')
38+
previous_version = merge_base = queue[h].id
39+
else:
40+
try:
41+
previous_version = repo.references[f'refs/tags/{previous_version}'].peel(pygit2.Commit).id
42+
except KeyError:
43+
print(f'Tag {previous_version} does not exist')
44+
sys.exit(1)
45+
merge_base = repo.merge_base(repo.head.peel(pygit2.Commit).id, previous_version)
46+
print(f'Merge base between HEAD and {sys.argv[1]} is {merge_base}')
47+
48+
# collect commits:
49+
# - from merge_base to HEAD
50+
# - from merge_base to previous_version
51+
our_commits = set(subprocess.run(['git', 'log', '--format=%H', f'{merge_base}..HEAD'], check=True, capture_output=True) \
52+
.stdout.decode().strip().split('\n'))
53+
if merge_base != previous_version:
54+
their_commits = subprocess.run(['git', 'log', '--format=%H', f'{merge_base}..{previous_version}'], check=True, capture_output=True) \
55+
.stdout.decode().strip().split('\n')
56+
else:
57+
their_commits = []
58+
59+
for commit in their_commits:
60+
if commit in our_commits:
61+
our_commits.remove(commit)
62+
commit = repo.get(commit)
63+
lines = commit.message.split('\n')
64+
r = re.compile(r'(?:cherry picked|backported) from commit ([0-9a-f]+)')
65+
for line in lines:
66+
m = r.search(line)
67+
if m:
68+
if m.group(1) not in our_commits:
69+
print(f'WARN: Cherry pick {m.group(1)} not found in our commits')
70+
else:
71+
print(f'Cherry pick: {m.group(1)}')
72+
our_commits.remove(m.group(1))
73+
74+
our_commits = [repo.get(commit) for commit in our_commits]
75+
our_commits.sort(key=lambda x: x.commit_time)
76+
77+
changelog_categories = {
78+
'BugFix': 'Bug fixes',
79+
'BuildFix': 'Build fixes',
80+
'BuildChange': 'Build changes',
81+
'NewFeature': 'New features',
82+
'Deprecation': 'Deprecations',
83+
'Uncategorized': 'Other changes',
84+
}
85+
86+
changelogs = {category: [] for category in changelog_categories}
87+
88+
for commit in our_commits:
89+
lines = commit.message.split('\n')
90+
related_issues = []
91+
r_related = re.compile(r'(?:Fixes|Related|Related-to):?\s+(.+)')
92+
for line in lines:
93+
m = r_related.fullmatch(line)
94+
if m:
95+
for issue in m.group(1).split(' '):
96+
related_issues.append(int(issue[1:]))
97+
elif line.startswith('Fixes') or line.startswith('Related'):
98+
print(f'WARN: Possibly invalid issue reference: {line}')
99+
elif line.startswith('Merge pull request #'):
100+
related_issues.append(int(line.split()[3][1:]))
101+
102+
changelog = [i for i, line in enumerate(lines) if line.startswith('ChangeLog:')]
103+
if len(changelog) > 1:
104+
print('WARN: Multiple Changelog lines')
105+
if not changelog:
106+
continue
107+
line = changelog[0] + 1
108+
changelog = lines[changelog[0]].removeprefix('ChangeLog:')
109+
while line < len(lines) and lines[line].strip() != '':
110+
changelog += ' ' + lines[line]
111+
line += 1
112+
cat, changelog = changelog.split(':', 1)
113+
cat = cat.strip()
114+
changelog = changelog.strip()
115+
if cat not in changelog_categories:
116+
print(f'WARN: Unknown category: {cat}')
117+
changelog = f'({cat}) {changelog}'
118+
cat = 'Uncategorized'
119+
if related_issues:
120+
changelog += f' ({" ".join(f"#{issue}" for issue in related_issues)})'
121+
122+
changelogs[cat].append(changelog)
123+
print(f'Commit {commit.id}: {changelog} [{cat}]')
124+
125+
# are we already on a tag?
126+
for ref in repo.references.iterator(pygit2.enums.ReferenceFilter.TAGS):
127+
if ref.shorthand.startswith('v') and ref.peel(pygit2.Commit) == repo.head.peel(pygit2.Commit):
128+
print(f'HEAD is already a release tag: {ref.shorthand}')
129+
sys.exit(1)
130+
131+
with tempfile.TemporaryDirectory() as tempdir:
132+
# can this commit be built?
133+
try:
134+
subprocess.run(['meson', 'setup', tempdir], check=True)
135+
subprocess.run(['ninja', '-C', tempdir], check=True)
136+
except subprocess.CalledProcessError:
137+
print('This commit cannot be built')
138+
sys.exit(1)
139+
140+
version = subprocess.run([Path(tempdir) / 'src' / 'picom', '--version'], capture_output=True, check=True).stdout.decode().strip()
141+
version = version.split(' ')[0]
142+
143+
if not version.startswith('v'):
144+
print('Version should start with "v"')
145+
sys.exit(1)
146+
147+
print(f'Version to be released is {version}')
148+
try:
149+
existing_tag = repo.references[f'refs/tags/{version}']
150+
print(f'Tag {version} already exists')
151+
sys.exit(1)
152+
except KeyError:
153+
pass
154+
155+
today = datetime.date.today()
156+
changelog = f'# {version} ({today.strftime("%Y-%b-%d")})\n'
157+
for category, changes in changelogs.items():
158+
if changes:
159+
changelog += f'\n## {changelog_categories[category]}\n\n'
160+
for change in changes:
161+
changelog += f'* {change}\n'
162+
changelog += '\n'
163+
164+
with open('CHANGELOG.md', 'r') as f:
165+
old = f.read()
166+
167+
with open('CHANGELOG.md', 'w') as f:
168+
f.write(changelog + old)
169+
170+
subprocess.run(['git', 'add', 'CHANGELOG.md'], check=True)
171+
subprocess.run(['git', 'commit', '-s', '-m', f'release: {version}'], check=True)
172+
subprocess.run(['git', 'tag', '-a', '-s', '-m', version, version], check=True)

0 commit comments

Comments
 (0)