Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit c1dbe84

Browse files
erikjohnstonrichvdhanoadragon453
authored
Add release helper script (#9713)
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Co-authored-by: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com>
1 parent 1d5f0e3 commit c1dbe84

File tree

3 files changed

+252
-0
lines changed

3 files changed

+252
-0
lines changed

changelog.d/9713.misc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add release helper script for automating part of the Synapse release process.

scripts-dev/release.py

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
# Copyright 2020 The Matrix.org Foundation C.I.C.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
"""An interactive script for doing a release. See `run()` below.
18+
"""
19+
20+
import subprocess
21+
import sys
22+
from typing import Optional
23+
24+
import click
25+
import git
26+
from packaging import version
27+
from redbaron import RedBaron
28+
29+
30+
@click.command()
31+
def run():
32+
"""An interactive script to walk through the initial stages of creating a
33+
release, including creating release branch, updating changelog and pushing to
34+
GitHub.
35+
36+
Requires the dev dependencies be installed, which can be done via:
37+
38+
pip install -e .[dev]
39+
40+
"""
41+
42+
# Make sure we're in a git repo.
43+
try:
44+
repo = git.Repo()
45+
except git.InvalidGitRepositoryError:
46+
raise click.ClickException("Not in Synapse repo.")
47+
48+
if repo.is_dirty():
49+
raise click.ClickException("Uncommitted changes exist.")
50+
51+
click.secho("Updating git repo...")
52+
repo.remote().fetch()
53+
54+
# Parse the AST and load the `__version__` node so that we can edit it
55+
# later.
56+
with open("synapse/__init__.py") as f:
57+
red = RedBaron(f.read())
58+
59+
version_node = None
60+
for node in red:
61+
if node.type != "assignment":
62+
continue
63+
64+
if node.target.type != "name":
65+
continue
66+
67+
if node.target.value != "__version__":
68+
continue
69+
70+
version_node = node
71+
break
72+
73+
if not version_node:
74+
print("Failed to find '__version__' definition in synapse/__init__.py")
75+
sys.exit(1)
76+
77+
# Parse the current version.
78+
current_version = version.parse(version_node.value.value.strip('"'))
79+
assert isinstance(current_version, version.Version)
80+
81+
# Figure out what sort of release we're doing and calcuate the new version.
82+
rc = click.confirm("RC", default=True)
83+
if current_version.pre:
84+
# If the current version is an RC we don't need to bump any of the
85+
# version numbers (other than the RC number).
86+
base_version = "{}.{}.{}".format(
87+
current_version.major,
88+
current_version.minor,
89+
current_version.micro,
90+
)
91+
92+
if rc:
93+
new_version = "{}.{}.{}rc{}".format(
94+
current_version.major,
95+
current_version.minor,
96+
current_version.micro,
97+
current_version.pre[1] + 1,
98+
)
99+
else:
100+
new_version = base_version
101+
else:
102+
# If this is a new release cycle then we need to know if its a major
103+
# version bump or a hotfix.
104+
release_type = click.prompt(
105+
"Release type",
106+
type=click.Choice(("major", "hotfix")),
107+
show_choices=True,
108+
default="major",
109+
)
110+
111+
if release_type == "major":
112+
base_version = new_version = "{}.{}.{}".format(
113+
current_version.major,
114+
current_version.minor + 1,
115+
0,
116+
)
117+
if rc:
118+
new_version = "{}.{}.{}rc1".format(
119+
current_version.major,
120+
current_version.minor + 1,
121+
0,
122+
)
123+
124+
else:
125+
base_version = new_version = "{}.{}.{}".format(
126+
current_version.major,
127+
current_version.minor,
128+
current_version.micro + 1,
129+
)
130+
if rc:
131+
new_version = "{}.{}.{}rc1".format(
132+
current_version.major,
133+
current_version.minor,
134+
current_version.micro + 1,
135+
)
136+
137+
# Confirm the calculated version is OK.
138+
if not click.confirm(f"Create new version: {new_version}?", default=True):
139+
click.get_current_context().abort()
140+
141+
# Switch to the release branch.
142+
release_branch_name = f"release-v{base_version}"
143+
release_branch = find_ref(repo, release_branch_name)
144+
if release_branch:
145+
if release_branch.is_remote():
146+
# If the release branch only exists on the remote we check it out
147+
# locally.
148+
repo.git.checkout(release_branch_name)
149+
release_branch = repo.active_branch
150+
else:
151+
# If a branch doesn't exist we create one. We ask which one branch it
152+
# should be based off, defaulting to sensible values depending on the
153+
# release type.
154+
if current_version.is_prerelease:
155+
default = release_branch_name
156+
elif release_type == "major":
157+
default = "develop"
158+
else:
159+
default = "master"
160+
161+
branch_name = click.prompt(
162+
"Which branch should the release be based on?", default=default
163+
)
164+
165+
base_branch = find_ref(repo, branch_name)
166+
if not base_branch:
167+
print(f"Could not find base branch {branch_name}!")
168+
click.get_current_context().abort()
169+
170+
# Check out the base branch and ensure it's up to date
171+
repo.head.reference = base_branch
172+
repo.head.reset(index=True, working_tree=True)
173+
if not base_branch.is_remote():
174+
update_branch(repo)
175+
176+
# Create the new release branch
177+
release_branch = repo.create_head(release_branch_name, commit=base_branch)
178+
179+
# Switch to the release branch and ensure its up to date.
180+
repo.git.checkout(release_branch_name)
181+
update_branch(repo)
182+
183+
# Update the `__version__` variable and write it back to the file.
184+
version_node.value = '"' + new_version + '"'
185+
with open("synapse/__init__.py", "w") as f:
186+
f.write(red.dumps())
187+
188+
# Generate changelogs
189+
subprocess.run("python3 -m towncrier", shell=True)
190+
191+
# Generate debian changelogs if its not an RC.
192+
if not rc:
193+
subprocess.run(
194+
f'dch -M -v {new_version} "New synapse release {new_version}."', shell=True
195+
)
196+
subprocess.run('dch -M -r -D stable ""', shell=True)
197+
198+
# Show the user the changes and ask if they want to edit the change log.
199+
repo.git.add("-u")
200+
subprocess.run("git diff --cached", shell=True)
201+
202+
if click.confirm("Edit changelog?", default=False):
203+
click.edit(filename="CHANGES.md")
204+
205+
# Commit the changes.
206+
repo.git.add("-u")
207+
repo.git.commit(f"-m {new_version}")
208+
209+
# We give the option to bail here in case the user wants to make sure things
210+
# are OK before pushing.
211+
if not click.confirm("Push branch to github?", default=True):
212+
print("")
213+
print("Run when ready to push:")
214+
print("")
215+
print(f"\tgit push -u {repo.remote().name} {repo.active_branch.name}")
216+
print("")
217+
sys.exit(0)
218+
219+
# Otherwise, push and open the changelog in the browser.
220+
repo.git.push("-u", repo.remote().name, repo.active_branch.name)
221+
222+
click.launch(
223+
f"https://github.com/matrix-org/synapse/blob/{repo.active_branch.name}/CHANGES.md"
224+
)
225+
226+
227+
def find_ref(repo: git.Repo, ref_name: str) -> Optional[git.HEAD]:
228+
"""Find the branch/ref, looking first locally then in the remote."""
229+
if ref_name in repo.refs:
230+
return repo.refs[ref_name]
231+
elif ref_name in repo.remote().refs:
232+
return repo.remote().refs[ref_name]
233+
else:
234+
return None
235+
236+
237+
def update_branch(repo: git.Repo):
238+
"""Ensure branch is up to date if it has a remote"""
239+
if repo.active_branch.tracking_branch():
240+
repo.git.merge(repo.active_branch.tracking_branch().name)
241+
242+
243+
if __name__ == "__main__":
244+
run()

setup.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,13 @@ def exec_file(path_segments):
103103
"flake8",
104104
]
105105

106+
CONDITIONAL_REQUIREMENTS["dev"] = CONDITIONAL_REQUIREMENTS["lint"] + [
107+
# The following are used by the release script
108+
"click==7.1.2",
109+
"redbaron==0.9.2",
110+
"GitPython==3.1.14",
111+
]
112+
106113
CONDITIONAL_REQUIREMENTS["mypy"] = ["mypy==0.812", "mypy-zope==0.2.13"]
107114

108115
# Dependencies which are exclusively required by unit test code. This is

0 commit comments

Comments
 (0)