Skip to content

Commit 7d2d6b0

Browse files
committed
refactor: update the release script
1 parent e90ac31 commit 7d2d6b0

2 files changed

Lines changed: 158 additions & 93 deletions

File tree

changelog.d/+b955ec60.changed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Update the release script `scripts/release.py`.

template/scripts/release.py.jinja

Lines changed: 157 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,36 @@
11
# /// script
2-
# requires-python = ">=3.10"
2+
# requires-python = "{% if dynamic_version %}>=3.10{% else %}>=3.11{% endif %}"
33
# dependencies = [
4+
{%- if dynamic_version %}
45
# "packaging>=24.2",
6+
{%- endif %}
57
# "towncrier>=24.8",
68
{%- if not dynamic_version %}
7-
# "uv>=0.7.7",
9+
# "uv>=0.8.6",
810
{%- endif %}
911
# ]
1012
# ///
11-
"""Automation for releases."""
13+
"""Make a release."""
1214

13-
# ruff: noqa: T201
15+
# ruff: noqa: S603, T201
1416

1517
from __future__ import annotations
1618

1719
import argparse
20+
{%- if not dynamic_version %}
21+
import enum
22+
{%- endif %}
23+
import json
1824
import subprocess
1925
import sys
26+
from contextlib import contextmanager
2027
from functools import partial
2128
from typing import TYPE_CHECKING
22-
29+
{% if dynamic_version %}
2330
from packaging.version import Version
24-
31+
{% endif %}
2532
if TYPE_CHECKING:
26-
from collections.abc import Sequence
33+
from collections.abc import Generator, Sequence
2734

2835
print_error = partial(print, file=sys.stderr)
2936

@@ -33,135 +40,192 @@ run = partial(
3340
stderr=subprocess.DEVNULL,
3441
)
3542

36-
37-
def main(argv: Sequence[str] | None = None) -> int: # noqa: PLR0911, PLR0915
38-
"""Prepare a new release."""
39-
# Parse command-line arguments.
40-
parser = argparse.ArgumentParser(description="prepare a new release")
43+
{% if not dynamic_version %}
44+
class Bump(enum.StrEnum):
45+
"""The enumeration of supported version bump semantics."""
46+
47+
MAJOR = enum.auto()
48+
MINOR = enum.auto()
49+
PATCH = enum.auto()
50+
STABLE = enum.auto()
51+
ALPHA = enum.auto()
52+
BETA = enum.auto()
53+
RC = enum.auto()
54+
POST = enum.auto()
55+
DEV = enum.auto()
56+
57+
{% endif %}
58+
def create_parser() -> argparse.ArgumentParser:
59+
"""Create the argument parser."""
60+
parser = argparse.ArgumentParser(description="make a release")
61+
{%- if dynamic_version %}
4162
parser.add_argument(
42-
"version",
63+
"--version",
4364
type=Version,
44-
help="provide the release version",
65+
help="provide the version",
4566
)
67+
{%- else %}
68+
mutex = parser.add_mutually_exclusive_group(required=True)
69+
mutex.add_argument(
70+
"--version",
71+
help="provide the version",
72+
)
73+
mutex.add_argument(
74+
"--bump",
75+
type=Bump,
76+
choices=Bump,
77+
help="update the version using the given semantics",
78+
)
79+
{%- endif %}
4680
parser.add_argument(
4781
"--dry-run",
4882
action="store_true",
49-
help="perform a dry run",
83+
help="do not push changes and reset the repository",
5084
)
51-
args = parser.parse_args(argv)
85+
return parser
5286

53-
# Get the public portion of the version.
54-
version = args.version.public
5587

56-
# Check if the Git repository is dirty.
88+
def check_repository() -> None:
89+
"""Check whether the repository is dirty."""
5790
try:
5891
run(("git", "diff", "--quiet"))
5992

6093
except subprocess.CalledProcessError:
6194
print_error("The Git repository is dirty.")
62-
return 1
95+
raise SystemExit(1) from None
96+
97+
{% if not dynamic_version %}
98+
def update_version(version: str | None = None, bump: str | None = None) -> str:
99+
"""Update the version and return the new version."""
100+
args = ["uv", "version", "--no-sync", "--output-format", "json"]
101+
102+
if version:
103+
args.append(version)
63104

64-
# Get the name of the base branch.
65-
base_branch = subprocess.check_output(
105+
if bump:
106+
args.extend(("--bump", bump))
107+
108+
try:
109+
version_json = subprocess.check_output(args)
110+
111+
except subprocess.CalledProcessError:
112+
print_error("An error occurred while bumping the version.")
113+
raise SystemExit(1) from None
114+
115+
version = json.loads(version_json)
116+
return version["version"]
117+
118+
{% endif %}
119+
def get_branch() -> str:
120+
"""Get the current branch."""
121+
return subprocess.check_output(
66122
("git", "rev-parse", "--abbrev-ref", "HEAD"),
67-
encoding="utf-8",
123+
text=True,
68124
).rstrip()
69125

70-
# Create the release branch.
71-
release_branch = f"release/{version}"
72126

127+
def create_branch(branch: str) -> None:
128+
"""Create a new branch."""
73129
try:
74-
run(("git", "branch", release_branch))
130+
run(("git", "branch", branch))
75131

76132
except subprocess.CalledProcessError:
77-
print_error(f"The release branch already exists: {release_branch!r}.")
78-
return 1
133+
print_error(f"The branch already exists: {branch!r}.")
134+
raise SystemExit(1) from None
135+
136+
print(f"Created new branch {branch!r}.")
79137

80-
print(f"Created release branch {release_branch!r}.")
81138

82-
# Switch to the release branch.
83-
run(("git", "checkout", release_branch))
139+
@contextmanager
140+
def switch_to_branch(branch: str) -> Generator[None]:
141+
"""Create a new branch and switch to it.
84142

85-
print(f"Switched from branch {base_branch!r} to release branch {release_branch!r}.")
143+
It is removed on exit.
144+
"""
145+
base_branch = get_branch()
146+
create_branch(branch)
86147

87-
# Bump the version.
88148
try:
89-
{%- if not dynamic_version %}
90-
run(("uv", "version", "--no-sync", version))
91-
{%- endif %}
92-
run(("towncrier", "build", "--yes", "--version", version))
149+
run(("git", "checkout", branch))
150+
print(f"Switched from branch {base_branch!r} to branch {branch!r}.")
151+
yield
93152

94-
except subprocess.CalledProcessError:
153+
finally:
95154
run(("git", "checkout", base_branch))
96-
run(("git", "branch", "-D", release_branch))
97-
print_error("An error occurred while bumping the version.")
98-
print_error(
99-
f"Removed release branch {release_branch!r} "
100-
f"and switched back to {base_branch!r}."
101-
)
102-
return 1
155+
run(("git", "branch", "-D", branch))
156+
print(f"Removed branch {branch!r} and switched back to {base_branch!r}.")
103157

104-
# Commit changes.
105-
{%- if not dynamic_version %}
106-
run(("git", "add", ":/pyproject.toml", ":/uv.lock"))
107-
{%- endif %}
108-
run(("git", "add", "-A", ":/changelog.d/*", ":/CHANGELOG.md"))
109-
run(("git", "commit", "-m", f"chore: prepare release {version}", "--no-verify"))
110158

111-
print(f"Committed changes on branch {release_branch!r}.")
159+
def build_changelog(version: str) -> None:
160+
"""Build the changelog."""
161+
try:
162+
run(("towncrier", "build", "--yes", "--version", version))
163+
164+
except subprocess.CalledProcessError:
165+
print_error("An error occurred while building the changelog.")
166+
raise SystemExit(1) from None
167+
112168

113-
# Create the release tag.
169+
def create_release_tag(version: str) -> str:
170+
"""Create the release tag."""
114171
release_tag = f"v{version}"
172+
message = f"bump version to {version}"
115173

116174
try:
117-
run(("git", "tag", "-a", release_tag, "-m", f"bump version to {version}"))
175+
run(("git", "tag", "-a", release_tag, "-m", message))
118176

119177
except subprocess.CalledProcessError:
120-
run(("git", "checkout", base_branch))
121-
run(("git", "branch", "-D", release_branch))
122178
print_error(f"The release tag already exists: {release_tag!r}.")
123-
print_error(
124-
f"Removed release branch {release_branch!r} "
125-
f"and switched back to {base_branch!r}."
126-
)
127-
return 1
179+
raise SystemExit(1) from None
128180

129181
print(f"Created release tag {release_tag!r}.")
182+
return release_tag
130183

131-
# Exit on dry run.
132-
if args.dry_run:
133-
print("Dry run success!")
134-
run(("git", "checkout", base_branch))
135-
run(("git", "branch", "-D", release_branch))
136-
print(
137-
f"Removed release branch {release_branch!r} "
138-
f"and switched back to {base_branch!r}."
139-
)
140-
run(("git", "tag", "-d", release_tag))
141-
print(f"Removed release tag {release_tag!r}.")
142-
return 0
143-
144-
# Push changes to the remote repository and push the release tag.
145-
try:
146-
run(("git", "push", "origin", f"{release_branch}:main", "--follow-tags"))
147184

148-
except subprocess.CalledProcessError:
149-
run(("git", "checkout", base_branch))
150-
run(("git", "branch", "-D", release_branch))
151-
print_error("An error occurred while pushing changes.")
152-
print_error(
153-
f"Removed release branch {release_branch!r} "
154-
f"and switched back to {base_branch!r}."
155-
)
156-
run(("git", "tag", "-d", release_tag))
157-
print_error(f"Removed release tag {release_tag!r}.")
158-
return 1
159-
160-
print(f"Pushed changes from {release_branch!r} to 'origin/main'.")
161-
162-
# Remove the release branch and switch to the main branch.
185+
def push_changes(branch: str, remote_branch: str = "main") -> None:
186+
"""Push the changes to the target branch."""
187+
run(("git", "push", "origin", f"{branch}:{remote_branch}", "--follow-tags"))
188+
print(f"Pushed changes from {branch!r} to 'origin/{remote_branch}'.")
189+
190+
191+
def main(argv: Sequence[str] | None = None) -> int:
192+
"""Prepare a new release."""
193+
parser = create_parser()
194+
args = parser.parse_args(argv)
195+
196+
check_repository()
197+
{% if dynamic_version %}
198+
version = args.version.public
199+
{% else %}
200+
version = update_version(args.version, args.bump)
201+
{% endif %}
202+
release_branch = f"release/{version}"
203+
204+
with switch_to_branch(release_branch):
205+
build_changelog(version)
206+
207+
# Commit changes.
208+
{%- if not dynamic_version %}
209+
run(("git", "add", ":/pyproject.toml", ":/uv.lock"))
210+
{%- endif %}
211+
run(("git", "add", "-A", ":/changelog.d/*", ":/CHANGELOG.md"))
212+
message = f"chore: prepare release {version}"
213+
run(("git", "commit", "--no-verify", "-m", message))
214+
print(f"Committed changes on branch {release_branch!r}.")
215+
216+
release_tag = create_release_tag(version)
217+
218+
# Exit on dry run.
219+
if args.dry_run:
220+
print("Dry run success!")
221+
run(("git", "tag", "-d", release_tag))
222+
print(f"Removed release tag {release_tag!r}.")
223+
return 0
224+
225+
push_changes(release_branch, "main")
226+
227+
# Switch to the main branch.
163228
run(("git", "checkout", "main"))
164-
run(("git", "branch", "-D", release_branch))
165229
run(("git", "fetch"))
166230
run(("git", "reset", "--hard", "origin/main"))
167231
return 0

0 commit comments

Comments
 (0)