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",
1010{% - endif %}
1111# ]
1212# ///
13- """Create a release ."""
13+ """Release a new version ."""
1414
15- # ruff: noqa: T201
15+ # ruff: noqa: S603, S607
1616
1717from __future__ import annotations
1818
@@ -21,128 +21,70 @@ import argparse
2121import json
2222{% - endif %}
2323import subprocess
24- import sys
25- from contextlib import contextmanager
26- from functools import partial
2724from typing import TYPE_CHECKING
2825{% if dynamic_version %}
2926from packaging.version import Version
3027{% endif %}
3128if 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
4332def 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
7154def 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-
14284def 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
15496def 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
164101def 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
180112def 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
185119def 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