From 9efb7a811a0ffa33a8f45ddeb1c8735adc9f0d78 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 30 Mar 2021 11:23:43 +0100 Subject: [PATCH 1/6] Add release helper script --- scripts-dev/release.py | 238 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100755 scripts-dev/release.py diff --git a/scripts-dev/release.py b/scripts-dev/release.py new file mode 100755 index 000000000000..8d099dc16791 --- /dev/null +++ b/scripts-dev/release.py @@ -0,0 +1,238 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright 2020 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""An interactive script to walk through the initial stages of creating a +release, including creating release branch, updating changelog and pushing to +GitHub. +""" + +import subprocess +import sys +from typing import Optional + +import click +import git +from packaging import version +from redbaron import RedBaron + + +def find_ref(repo: git.Repo, ref_name: str) -> Optional[git.HEAD]: + """Find the branch/ref, looking first locally then in the remote.""" + if ref_name in repo.refs: + return repo.refs[ref_name] + elif ref_name in repo.remote().refs: + return repo.remote().refs[ref_name] + else: + return None + + +def update_branch(repo: git.Repo): + """Ensure branch is up to date if it has a remote""" + if repo.active_branch.tracking_branch(): + repo.git.merge(repo.active_branch.tracking_branch().name) + + +@click.command() +def release(): + """Main release command""" + + # Make sure we're in a git repo. + try: + repo = git.Repo() + except git.InvalidGitRepositoryError: + raise click.ClickException("Not in Synapse repo.") + + if repo.is_dirty(): + raise click.ClickException("Uncommitted changes exist.") + + click.secho("Updating git repo...") + repo.remote().fetch() + + # Parse the AST and load the `__version__` node so that we can edit it + # later. + with open("synapse/__init__.py") as f: + red = RedBaron(f.read()) + + version_node = None + for node in red: + if node.type != "assignment": + continue + + if node.target.type != "name": + continue + + if node.target.value != "__version__": + continue + + version_node = node + break + + if not version_node: + print("Failed to find '__version__' definition in synapse/__init__.py") + sys.exit(1) + + # Parse the current version. + current_version = version.parse(version_node.value.value.strip('"')) + assert isinstance(current_version, version.Version) + + # Figure out what sort of release we're doing and calcuate the new version. + rc = click.confirm("RC", default=True) + if current_version.pre: + # If the current version is an RC we don't need to bump any of the + # version numbers (other than the RC number). + base_version = "{}.{}.{}".format( + current_version.major, + current_version.minor, + current_version.micro, + ) + + if rc: + new_version = "{}.{}.{}rc{}".format( + current_version.major, + current_version.minor, + current_version.micro, + current_version.pre[1] + 1, + ) + else: + new_version = base_version + else: + # If this is a new release cycle then we need to know if its a major + # version bump or a hotfix. + release_type = click.prompt( + "Release type", + type=click.Choice(("major", "hotfix")), + show_choices=True, + default="major", + ) + + if release_type == "major": + base_version = new_version = "{}.{}.{}".format( + current_version.major, + current_version.minor + 1, + 0, + ) + if rc: + new_version = "{}.{}.{}rc1".format( + current_version.major, + current_version.minor + 1, + 0, + ) + + else: + base_version = new_version = "{}.{}.{}".format( + current_version.major, + current_version.minor, + current_version.micro + 1, + ) + if rc: + new_version = "{}.{}.{}rc1".format( + current_version.major, + current_version.minor, + current_version.micro + 1, + ) + + # Confirm the calculated version is OK. + if not click.confirm(f"Create new version: {new_version}?", default=True): + click.get_current_context().abort() + + # Switch to the release branch. + release_branch_name = f"release-v{base_version}" + release_branch = find_ref(repo, release_branch_name) + if release_branch: + if release_branch.is_remote(): + # If the release branch only exists on the remote we check it out + # locally. + repo.git.checkout(release_branch_name) + release_branch = repo.active_branch + else: + # If a branch doesn't exist we create one. We ask which one branch it + # should be based off, defaulting to sensible values depending on the + # release type. + if current_version.is_prerelease: + default = release_branch_name + elif release_type == "major": + default = "develop" + else: + default = "master" + + branch_name = click.prompt( + "Which branch should release be based off of?", default=default + ) + + base_branch = find_ref(repo, branch_name) + if not base_branch: + print(f"Could not find base branch {branch_name}!") + click.get_current_context().abort() + + # Checkout the base branch and ensure its up to date + repo.head.reference = base_branch + repo.head.reset(index=True, working_tree=True) + if not base_branch.is_remote(): + update_branch(repo) + + # Create the new release branch + release_branch = repo.create_head(release_branch_name, commit=base_branch) + + # Switch to the release branch and ensure its up to date. + repo.git.checkout(release_branch_name) + update_branch(repo) + + # Update the `__version__` variable and write it back to the file. + version_node.value = '"' + new_version + '"' + with open("synapse/__init__.py", "w") as f: + f.write(red.dumps()) + + # Generate changelgs + subprocess.run("python3 -m towncrier", shell=True) + + # Generate debian changelogs if its not an RC. + if not rc: + subprocess.run( + f'dch -M -v {new_version} "New synapse release {new_version}."', shell=True + ) + subprocess.run('dch -M -r -D stable ""', shell=True) + + # Show the user the changes and ask if they want to edit the change log. + repo.git.add("-u") + subprocess.run("git diff --cached", shell=True) + + if click.confirm("Edit changelog?", default=False): + click.edit(filename="CHANGES.md") + + # Commit the changes. + repo.git.add("-u") + repo.git.commit(f"-m {new_version}") + + # We give the option to bail here in case the user wants to make sure things + # are OK before pushing. + if not click.confirm("Push branch to github?", default=True): + print("") + print("Run when ready to push:") + print("") + print(f"\tgit push -u {repo.remote().name} {repo.active_branch.name}") + print("") + sys.exit(0) + + # Otherwise, push and open the changelog in the browser. + repo.git.push("-u", repo.remote().name, repo.active_branch.name) + + click.launch( + f"https://github.com/matrix-org/synapse/blob/{repo.active_branch.name}/CHANGES.md" + ) + + +if __name__ == "__main__": + release() From f9a3089d6b8c6218ac364ee452b843dbe0bc55ac Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 30 Mar 2021 14:20:15 +0100 Subject: [PATCH 2/6] Newsfile --- changelog.d/9713.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/9713.misc diff --git a/changelog.d/9713.misc b/changelog.d/9713.misc new file mode 100644 index 000000000000..908e7a24595b --- /dev/null +++ b/changelog.d/9713.misc @@ -0,0 +1 @@ +Add release helper script for automating part of the Synapse release process. From e588f2a4af86676af7a616cb0e333d571045c185 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 1 Apr 2021 13:11:29 +0100 Subject: [PATCH 3/6] Fix wording Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --- scripts-dev/release.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts-dev/release.py b/scripts-dev/release.py index 8d099dc16791..aeed038a1223 100755 --- a/scripts-dev/release.py +++ b/scripts-dev/release.py @@ -169,7 +169,7 @@ def release(): default = "master" branch_name = click.prompt( - "Which branch should release be based off of?", default=default + "Which branch should release be based on?", default=default ) base_branch = find_ref(repo, branch_name) @@ -177,7 +177,7 @@ def release(): print(f"Could not find base branch {branch_name}!") click.get_current_context().abort() - # Checkout the base branch and ensure its up to date + # Check out the base branch and ensure it's up to date repo.head.reference = base_branch repo.head.reset(index=True, working_tree=True) if not base_branch.is_remote(): From a48b5abf812b7dbc16034e8a5bcd407db3048057 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 1 Apr 2021 13:23:39 +0100 Subject: [PATCH 4/6] Add 'dev' dependency section --- setup.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/setup.py b/setup.py index 1939a7b86b5c..852ca019f902 100755 --- a/setup.py +++ b/setup.py @@ -103,6 +103,13 @@ def exec_file(path_segments): "flake8", ] +CONDITIONAL_REQUIREMENTS["dev"] = CONDITIONAL_REQUIREMENTS["lint"] + [ + # The following are used by the release script + "click==7.1.2", + "redbaron==0.9.2", + "GitPython==3.1.14", +] + CONDITIONAL_REQUIREMENTS["mypy"] = ["mypy==0.812", "mypy-zope==0.2.13"] # Dependencies which are exclusively required by unit test code. This is From 222bc38dbb8dba11e1152ce39dcf14da99379550 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 6 Apr 2021 10:11:08 +0100 Subject: [PATCH 5/6] Apply suggestions from code review Co-authored-by: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> --- scripts-dev/release.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts-dev/release.py b/scripts-dev/release.py index aeed038a1223..d863edbd942e 100755 --- a/scripts-dev/release.py +++ b/scripts-dev/release.py @@ -169,7 +169,7 @@ def release(): default = "master" branch_name = click.prompt( - "Which branch should release be based on?", default=default + "Which branch should the release be based on?", default=default ) base_branch = find_ref(repo, branch_name) @@ -195,7 +195,7 @@ def release(): with open("synapse/__init__.py", "w") as f: f.write(red.dumps()) - # Generate changelgs + # Generate changelogs subprocess.run("python3 -m towncrier", shell=True) # Generate debian changelogs if its not an RC. From a03344cda67b7bbac56e9bc43da8ac563831fff0 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 12 Apr 2021 17:01:22 +0100 Subject: [PATCH 6/6] Add better description when running --help --- scripts-dev/release.py | 46 ++++++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/scripts-dev/release.py b/scripts-dev/release.py index d863edbd942e..1042fa48bc8c 100755 --- a/scripts-dev/release.py +++ b/scripts-dev/release.py @@ -14,9 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""An interactive script to walk through the initial stages of creating a -release, including creating release branch, updating changelog and pushing to -GitHub. +"""An interactive script for doing a release. See `run()` below. """ import subprocess @@ -29,25 +27,17 @@ from redbaron import RedBaron -def find_ref(repo: git.Repo, ref_name: str) -> Optional[git.HEAD]: - """Find the branch/ref, looking first locally then in the remote.""" - if ref_name in repo.refs: - return repo.refs[ref_name] - elif ref_name in repo.remote().refs: - return repo.remote().refs[ref_name] - else: - return None - +@click.command() +def run(): + """An interactive script to walk through the initial stages of creating a + release, including creating release branch, updating changelog and pushing to + GitHub. -def update_branch(repo: git.Repo): - """Ensure branch is up to date if it has a remote""" - if repo.active_branch.tracking_branch(): - repo.git.merge(repo.active_branch.tracking_branch().name) + Requires the dev dependencies be installed, which can be done via: + pip install -e .[dev] -@click.command() -def release(): - """Main release command""" + """ # Make sure we're in a git repo. try: @@ -234,5 +224,21 @@ def release(): ) +def find_ref(repo: git.Repo, ref_name: str) -> Optional[git.HEAD]: + """Find the branch/ref, looking first locally then in the remote.""" + if ref_name in repo.refs: + return repo.refs[ref_name] + elif ref_name in repo.remote().refs: + return repo.remote().refs[ref_name] + else: + return None + + +def update_branch(repo: git.Repo): + """Ensure branch is up to date if it has a remote""" + if repo.active_branch.tracking_branch(): + repo.git.merge(repo.active_branch.tracking_branch().name) + + if __name__ == "__main__": - release() + run()