Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit a7baccc

Browse files
authoredAug 3, 2021
Extend the release script to tag and create the releases. (#10496)
1 parent 2bae2c6 commit a7baccc

File tree

3 files changed

+278
-36
lines changed

3 files changed

+278
-36
lines changed
 

‎changelog.d/10496.misc

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Extend release script to also tag and create GitHub releases.

‎scripts-dev/release.py

+275-36
Original file line numberDiff line numberDiff line change
@@ -14,29 +14,57 @@
1414
# See the License for the specific language governing permissions and
1515
# limitations under the License.
1616

17-
"""An interactive script for doing a release. See `run()` below.
17+
"""An interactive script for doing a release. See `cli()` below.
1818
"""
1919

20+
import re
2021
import subprocess
2122
import sys
22-
from typing import Optional
23+
import urllib.request
24+
from os import path
25+
from tempfile import TemporaryDirectory
26+
from typing import List, Optional, Tuple
2327

28+
import attr
2429
import click
30+
import commonmark
2531
import git
32+
import redbaron
33+
from click.exceptions import ClickException
34+
from github import Github
2635
from packaging import version
27-
from redbaron import RedBaron
2836

2937

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.
38+
@click.group()
39+
def cli():
40+
"""An interactive script to walk through the parts of creating a release.
3541
3642
Requires the dev dependencies be installed, which can be done via:
3743
3844
pip install -e .[dev]
3945
46+
Then to use:
47+
48+
./scripts-dev/release.py prepare
49+
50+
# ... ask others to look at the changelog ...
51+
52+
./scripts-dev/release.py tag
53+
54+
# ... wait for asssets to build ...
55+
56+
./scripts-dev/release.py publish
57+
./scripts-dev/release.py upload
58+
59+
If the env var GH_TOKEN (or GITHUB_TOKEN) is set, or passed into the
60+
`tag`/`publish` command, then a new draft release will be created/published.
61+
"""
62+
63+
64+
@cli.command()
65+
def prepare():
66+
"""Do the initial stages of creating a release, including creating release
67+
branch, updating changelog and pushing to GitHub.
4068
"""
4169

4270
# Make sure we're in a git repo.
@@ -51,32 +79,8 @@ def run():
5179
click.secho("Updating git repo...")
5280
repo.remote().fetch()
5381

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)
82+
# Get the current version and AST from root Synapse module.
83+
current_version, parsed_synapse_ast, version_node = parse_version_from_module()
8084

8185
# Figure out what sort of release we're doing and calcuate the new version.
8286
rc = click.confirm("RC", default=True)
@@ -190,7 +194,7 @@ def run():
190194
# Update the `__version__` variable and write it back to the file.
191195
version_node.value = '"' + new_version + '"'
192196
with open("synapse/__init__.py", "w") as f:
193-
f.write(red.dumps())
197+
f.write(parsed_synapse_ast.dumps())
194198

195199
# Generate changelogs
196200
subprocess.run("python3 -m towncrier", shell=True)
@@ -240,6 +244,180 @@ def run():
240244
)
241245

242246

247+
@cli.command()
248+
@click.option("--gh-token", envvar=["GH_TOKEN", "GITHUB_TOKEN"])
249+
def tag(gh_token: Optional[str]):
250+
"""Tags the release and generates a draft GitHub release"""
251+
252+
# Make sure we're in a git repo.
253+
try:
254+
repo = git.Repo()
255+
except git.InvalidGitRepositoryError:
256+
raise click.ClickException("Not in Synapse repo.")
257+
258+
if repo.is_dirty():
259+
raise click.ClickException("Uncommitted changes exist.")
260+
261+
click.secho("Updating git repo...")
262+
repo.remote().fetch()
263+
264+
# Find out the version and tag name.
265+
current_version, _, _ = parse_version_from_module()
266+
tag_name = f"v{current_version}"
267+
268+
# Check we haven't released this version.
269+
if tag_name in repo.tags:
270+
raise click.ClickException(f"Tag {tag_name} already exists!\n")
271+
272+
# Get the appropriate changelogs and tag.
273+
changes = get_changes_for_version(current_version)
274+
275+
click.echo_via_pager(changes)
276+
if click.confirm("Edit text?", default=False):
277+
changes = click.edit(changes, require_save=False)
278+
279+
repo.create_tag(tag_name, message=changes)
280+
281+
if not click.confirm("Push tag to GitHub?", default=True):
282+
print("")
283+
print("Run when ready to push:")
284+
print("")
285+
print(f"\tgit push {repo.remote().name} tag {current_version}")
286+
print("")
287+
return
288+
289+
repo.git.push(repo.remote().name, "tag", tag_name)
290+
291+
# If no token was given, we bail here
292+
if not gh_token:
293+
click.launch(f"https://github.com/matrix-org/synapse/releases/edit/{tag_name}")
294+
return
295+
296+
# Create a new draft release
297+
gh = Github(gh_token)
298+
gh_repo = gh.get_repo("matrix-org/synapse")
299+
release = gh_repo.create_git_release(
300+
tag=tag_name,
301+
name=tag_name,
302+
message=changes,
303+
draft=True,
304+
prerelease=current_version.is_prerelease,
305+
)
306+
307+
# Open the release and the actions where we are building the assets.
308+
click.launch(release.url)
309+
click.launch(
310+
f"https://github.com/matrix-org/synapse/actions?query=branch%3A{tag_name}"
311+
)
312+
313+
click.echo("Wait for release assets to be built")
314+
315+
316+
@cli.command()
317+
@click.option("--gh-token", envvar=["GH_TOKEN", "GITHUB_TOKEN"], required=True)
318+
def publish(gh_token: str):
319+
"""Publish release."""
320+
321+
# Make sure we're in a git repo.
322+
try:
323+
repo = git.Repo()
324+
except git.InvalidGitRepositoryError:
325+
raise click.ClickException("Not in Synapse repo.")
326+
327+
if repo.is_dirty():
328+
raise click.ClickException("Uncommitted changes exist.")
329+
330+
current_version, _, _ = parse_version_from_module()
331+
tag_name = f"v{current_version}"
332+
333+
if not click.confirm(f"Publish {tag_name}?", default=True):
334+
return
335+
336+
# Publish the draft release
337+
gh = Github(gh_token)
338+
gh_repo = gh.get_repo("matrix-org/synapse")
339+
for release in gh_repo.get_releases():
340+
if release.title == tag_name:
341+
break
342+
else:
343+
raise ClickException(f"Failed to find GitHub release for {tag_name}")
344+
345+
assert release.title == tag_name
346+
347+
if not release.draft:
348+
click.echo("Release already published.")
349+
return
350+
351+
release = release.update_release(
352+
name=release.title,
353+
message=release.body,
354+
tag_name=release.tag_name,
355+
prerelease=release.prerelease,
356+
draft=False,
357+
)
358+
359+
360+
@cli.command()
361+
def upload():
362+
"""Upload release to pypi."""
363+
364+
current_version, _, _ = parse_version_from_module()
365+
tag_name = f"v{current_version}"
366+
367+
pypi_asset_names = [
368+
f"matrix_synapse-{current_version}-py3-none-any.whl",
369+
f"matrix-synapse-{current_version}.tar.gz",
370+
]
371+
372+
with TemporaryDirectory(prefix=f"synapse_upload_{tag_name}_") as tmpdir:
373+
for name in pypi_asset_names:
374+
filename = path.join(tmpdir, name)
375+
url = f"https://github.com/matrix-org/synapse/releases/download/{tag_name}/{name}"
376+
377+
click.echo(f"Downloading {name} into {filename}")
378+
urllib.request.urlretrieve(url, filename=filename)
379+
380+
if click.confirm("Upload to PyPI?", default=True):
381+
subprocess.run("twine upload *", shell=True, cwd=tmpdir)
382+
383+
click.echo(
384+
f"Done! Remember to merge the tag {tag_name} into the appropriate branches"
385+
)
386+
387+
388+
def parse_version_from_module() -> Tuple[
389+
version.Version, redbaron.RedBaron, redbaron.Node
390+
]:
391+
# Parse the AST and load the `__version__` node so that we can edit it
392+
# later.
393+
with open("synapse/__init__.py") as f:
394+
red = redbaron.RedBaron(f.read())
395+
396+
version_node = None
397+
for node in red:
398+
if node.type != "assignment":
399+
continue
400+
401+
if node.target.type != "name":
402+
continue
403+
404+
if node.target.value != "__version__":
405+
continue
406+
407+
version_node = node
408+
break
409+
410+
if not version_node:
411+
print("Failed to find '__version__' definition in synapse/__init__.py")
412+
sys.exit(1)
413+
414+
# Parse the current version.
415+
current_version = version.parse(version_node.value.value.strip('"'))
416+
assert isinstance(current_version, version.Version)
417+
418+
return current_version, red, version_node
419+
420+
243421
def find_ref(repo: git.Repo, ref_name: str) -> Optional[git.HEAD]:
244422
"""Find the branch/ref, looking first locally then in the remote."""
245423
if ref_name in repo.refs:
@@ -256,5 +434,66 @@ def update_branch(repo: git.Repo):
256434
repo.git.merge(repo.active_branch.tracking_branch().name)
257435

258436

437+
def get_changes_for_version(wanted_version: version.Version) -> str:
438+
"""Get the changelogs for the given version.
439+
440+
If an RC then will only get the changelog for that RC version, otherwise if
441+
its a full release will get the changelog for the release and all its RCs.
442+
"""
443+
444+
with open("CHANGES.md") as f:
445+
changes = f.read()
446+
447+
# First we parse the changelog so that we can split it into sections based
448+
# on the release headings.
449+
ast = commonmark.Parser().parse(changes)
450+
451+
@attr.s(auto_attribs=True)
452+
class VersionSection:
453+
title: str
454+
455+
# These are 0-based.
456+
start_line: int
457+
end_line: Optional[int] = None # Is none if its the last entry
458+
459+
headings: List[VersionSection] = []
460+
for node, _ in ast.walker():
461+
# We look for all text nodes that are in a level 1 heading.
462+
if node.t != "text":
463+
continue
464+
465+
if node.parent.t != "heading" or node.parent.level != 1:
466+
continue
467+
468+
# If we have a previous heading then we update its `end_line`.
469+
if headings:
470+
headings[-1].end_line = node.parent.sourcepos[0][0] - 1
471+
472+
headings.append(VersionSection(node.literal, node.parent.sourcepos[0][0] - 1))
473+
474+
changes_by_line = changes.split("\n")
475+
476+
version_changelog = [] # The lines we want to include in the changelog
477+
478+
# Go through each section and find any that match the requested version.
479+
regex = re.compile(r"^Synapse v?(\S+)")
480+
for section in headings:
481+
groups = regex.match(section.title)
482+
if not groups:
483+
continue
484+
485+
heading_version = version.parse(groups.group(1))
486+
heading_base_version = version.parse(heading_version.base_version)
487+
488+
# Check if heading version matches the requested version, or if its an
489+
# RC of the requested version.
490+
if wanted_version not in (heading_version, heading_base_version):
491+
continue
492+
493+
version_changelog.extend(changes_by_line[section.start_line : section.end_line])
494+
495+
return "\n".join(version_changelog)
496+
497+
259498
if __name__ == "__main__":
260-
run()
499+
cli()

‎setup.py

+2
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ def exec_file(path_segments):
108108
"click==7.1.2",
109109
"redbaron==0.9.2",
110110
"GitPython==3.1.14",
111+
"commonmark==0.9.1",
112+
"pygithub==1.55",
111113
]
112114

113115
CONDITIONAL_REQUIREMENTS["mypy"] = ["mypy==0.812", "mypy-zope==0.2.13"]

0 commit comments

Comments
 (0)
This repository has been archived.