Skip to content

Commit 1eb5fe4

Browse files
committed
refactor: improve the release script
1 parent dafe52a commit 1eb5fe4

2 files changed

Lines changed: 83 additions & 134 deletions

File tree

changelog.d/+626d22d0.changed.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Make `scripts/release.py` create GitHub releases.
1+
Improve `scripts/release.py`. It can now pass arguments or options to `uv version` and create GitHub releases.

template/scripts/release.py.jinja

Lines changed: 82 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# /// script
2-
# requires-python = "{% if dynamic_version %}>=3.10{% else %}>=3.11{% endif %}"
2+
# requires-python = ">=3.10"
33
# dependencies = [
44
{%- if dynamic_version %}
55
# "packaging>=24.2",
@@ -10,9 +10,9 @@
1010
{%- endif %}
1111
# ]
1212
# ///
13-
"""Create a release."""
13+
"""Release a new version."""
1414

15-
# ruff: noqa: T201
15+
# ruff: noqa: S603, S607
1616

1717
from __future__ import annotations
1818

@@ -21,128 +21,70 @@ import argparse
2121
import json
2222
{%- endif %}
2323
import subprocess
24-
import sys
25-
from contextlib import contextmanager
26-
from functools import partial
2724
from typing import TYPE_CHECKING
2825
{% if dynamic_version %}
2926
from packaging.version import Version
3027
{% endif %}
3128
if TYPE_CHECKING:
32-
from collections.abc import Generator, Sequence
33-
34-
print_err = partial(print, file=sys.stderr)
35-
36-
run = partial(
37-
subprocess.check_call,
38-
stdout=subprocess.DEVNULL,
39-
stderr=subprocess.DEVNULL,
40-
)
29+
from collections.abc import Sequence
4130

4231

4332
def create_parser() -> argparse.ArgumentParser:
44-
"""Create the argument parser."""
45-
parser = argparse.ArgumentParser(description="make a release")
46-
{%- if dynamic_version %}
47-
parser.add_argument(
48-
"--version",
49-
type=Version,
50-
help="provide the version",
51-
)
52-
{%- else %}
53-
mutex = parser.add_mutually_exclusive_group(required=True)
54-
mutex.add_argument(
55-
"--version",
56-
help="provide the version",
57-
)
58-
mutex.add_argument(
59-
"--bump",
60-
help="update the version using the given semantics",
33+
"""Return the argument parser."""
34+
parser = argparse.ArgumentParser(
35+
description="release a new version",
6136
)
62-
{%- endif %}
6337
parser.add_argument(
6438
"--dry-run",
6539
action="store_true",
66-
help="do not push changes and reset the repository",
40+
help="do not update the origin remote and reset the local repository",
41+
)
42+
{%- if dynamic_version %}
43+
parser.add_argument("version", type=Version, help="provide the version")
44+
{%- else %}
45+
parser.add_argument(
46+
"version_args",
47+
nargs=argparse.PARSER,
48+
help="arguments or options to pass to the uv version command",
6749
)
50+
{%- endif %}
6851
return parser
6952

7053

7154
def check_repository() -> None:
72-
"""Check whether the repository is dirty."""
73-
try:
74-
run(("git", "diff", "--quiet"))
75-
76-
except subprocess.CalledProcessError:
77-
print_err("The Git repository is dirty.")
78-
raise SystemExit(1) from None
55+
"""Check whether the local repository is dirty or clean."""
56+
subprocess.check_call(["git", "diff", "--exit-code"])
7957

8058
{% if not dynamic_version %}
81-
def update_version(version: str | None = None, bump: str | None = None) -> str:
82-
"""Update the version and return the new version."""
83-
cmd = ["uv", "version", "--no-sync", "--output-format", "json"]
84-
85-
if version:
86-
cmd.append(version)
87-
88-
if bump:
89-
cmd.extend(("--bump", bump))
90-
91-
try:
92-
version_json = subprocess.check_output(cmd) # noqa: S603
93-
94-
except subprocess.CalledProcessError:
95-
print_err("An error occurred while bumping the version.")
96-
raise SystemExit(1) from None
97-
59+
def update_version(args: Sequence[str] | None) -> str:
60+
"""Update and return the version."""
61+
# Update the version.
62+
version_json = subprocess.check_output(
63+
[
64+
"uv",
65+
"version",
66+
"--no-sync",
67+
"--output-format",
68+
"json",
69+
*args,
70+
],
71+
)
9872
version = json.loads(version_json)
9973
return version["version"]
10074

10175
{% endif %}
102-
def get_branch() -> str:
103-
"""Get the current branch."""
76+
def get_current_branch() -> str:
77+
"""Get the current Git branch."""
10478
return subprocess.check_output(
105-
("git", "rev-parse", "--abbrev-ref", "HEAD"),
79+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
10680
text=True,
10781
).rstrip()
10882

10983

110-
def create_branch(branch: str) -> None:
111-
"""Create a new branch."""
112-
try:
113-
run(("git", "branch", branch))
114-
115-
except subprocess.CalledProcessError:
116-
print_err(f"The branch already exists: {branch!r}.")
117-
raise SystemExit(1) from None
118-
119-
print(f"Created new branch {branch!r}.")
120-
121-
122-
@contextmanager
123-
def switch_to_branch(branch: str) -> Generator[None]:
124-
"""Create a new branch and switch to it.
125-
126-
It is removed on exit.
127-
"""
128-
base_branch = get_branch()
129-
create_branch(branch)
130-
131-
try:
132-
run(("git", "checkout", branch))
133-
print(f"Switched from branch {base_branch!r} to branch {branch!r}.")
134-
yield
135-
136-
finally:
137-
run(("git", "checkout", base_branch))
138-
run(("git", "branch", "-D", branch))
139-
print(f"Removed branch {branch!r} and switched back to {base_branch!r}.")
140-
141-
14284
def get_release_notes(version: str) -> str:
14385
"""Return the release notes."""
14486
release_notes = subprocess.check_output(
145-
("towncrier", "build", "--version", version, "--draft"),
87+
["towncrier", "build", "--version", version, "--draft"],
14688
stderr=subprocess.DEVNULL,
14789
text=True,
14890
).rstrip()
@@ -153,82 +95,89 @@ def get_release_notes(version: str) -> str:
15395

15496
def update_changelog(version: str) -> None:
15597
"""Update the changelog."""
156-
try:
157-
run(("towncrier", "build", "--version", version, "--yes"))
158-
159-
except subprocess.CalledProcessError:
160-
print_err("An error occurred while building the changelog.")
161-
raise SystemExit(1) from None
98+
subprocess.check_call(["towncrier", "build", "--version", version, "--yes"])
16299

163100

164101
def create_release_tag(version: str) -> str:
165-
"""Create the release tag."""
102+
"""Create and return the release tag."""
166103
release_tag = f"v{version}"
167104
message = f"bump version to {version}"
168-
169-
try:
170-
run(("git", "tag", "-a", release_tag, "-m", message))
171-
172-
except subprocess.CalledProcessError:
173-
print_err(f"The release tag already exists: {release_tag!r}.")
174-
raise SystemExit(1) from None
175-
176-
print(f"Created release tag {release_tag!r}.")
105+
# Make sure to create an annotated tag.
106+
subprocess.check_call(
107+
["git", "tag", "--annotate", release_tag, "--message", message],
108+
)
177109
return release_tag
178110

179111

180112
def create_release(release_tag: str, release_notes: str) -> None:
181-
"""Create the GitHub release."""
182-
run(("gh", "release", "create", release_tag, "--notes", release_notes))
113+
"""Create the GitHub release with release notes."""
114+
subprocess.check_call(
115+
["gh", "release", "create", release_tag, "--notes", release_notes]
116+
)
183117

184118

185119
def main(argv: Sequence[str] | None = None) -> int:
186-
"""Create a release."""
120+
"""Release a new version."""
121+
# Parse command-line arguments.
187122
parser = create_parser()
188123
args = parser.parse_args(argv)
189124

125+
# Check whether the local repository is dirty or clean.
190126
check_repository()
191127
{% if dynamic_version %}
192-
version = args.version.public
128+
version = str(args.version)
193129
{% else %}
194-
version = update_version(args.version, args.bump)
130+
# Run the uv version command to update the version.
131+
version = update_version(args.uv_version_args)
195132
{% endif %}
196133
release_branch = f"release/{version}"
134+
base_branch = get_current_branch()
135+
136+
try:
137+
# Create the release branch and switch to it.
138+
subprocess.check_call(["git", "switch", "--create", release_branch])
197139

198-
with switch_to_branch(release_branch):
140+
# Update the changelog.
199141
release_notes = get_release_notes(version)
200142
update_changelog(version)
201143

144+
# Add all changes as the local repository was clean.
145+
subprocess.check_call(["git", "add", "--all", "."])
146+
202147
# Commit changes.
203-
{%- if not dynamic_version %}
204-
run(("git", "add", ":/pyproject.toml", ":/uv.lock"))
205-
{%- endif %}
206-
run(("git", "add", "-A", ":/changelog.d", ":/CHANGELOG.md"))
207148
message = f"chore: prepare release {version}"
208-
run(("git", "commit", "--no-verify", "-m", message))
209-
print(f"Committed changes on branch {release_branch!r}.")
149+
subprocess.check_call(["git", "commit", "--no-verify", "--message", message])
210150

151+
# Create the release tag.
211152
release_tag = create_release_tag(version)
212153

213-
# Exit on dry run.
214154
if args.dry_run:
215-
print("Dry run success!")
216-
run(("git", "tag", "-d", release_tag))
217-
print(f"Removed release tag {release_tag!r}.")
155+
# Remove the release tag.
156+
subprocess.check_call(["git", "tag", "--delete", release_tag])
218157
return 0
219158

220-
run(
221-
("git", "push", "--atomic", "origin", f"{release_branch}:main", release_tag)
159+
# Push the release branch and the release tag to the origin remote.
160+
# The local release branch is pushed to the remote main branch.
161+
subprocess.check_call(
162+
["git", "push", "--atomic", "origin", f"{release_branch}:main", release_tag]
222163
)
223-
print(f"Pushed changes from branch {release_branch!r} to branch 'origin/main'.")
224-
print(f"Pushed release tag {release_tag!r} to the origin remote.")
225164

165+
# Create the GitHub release.
226166
create_release(release_tag, release_notes)
227167

168+
finally:
169+
# Switch back to the base Git branch.
170+
subprocess.check_call(["git", "checkout", base_branch])
171+
# Remove the release branch.
172+
subprocess.check_call(["git", "branch", "--delete", "--force", release_branch])
173+
174+
# Fetch all changes from the remote main branch.
175+
subprocess.check_call(["git", "fetch", "origin", "main"])
228176
# Switch to the main branch.
229-
run(("git", "checkout", "main"))
230-
run(("git", "fetch"))
231-
run(("git", "reset", "--hard", "origin/main"))
177+
subprocess.check_call(["git", "switch", "main"])
178+
# Pull changes from the remote main branch.
179+
subprocess.check_call(["git", "pull", "--ff-only", "origin", "main"])
180+
232181
return 0
233182

234183

0 commit comments

Comments
 (0)