Skip to content

Commit 73b65e4

Browse files
author
Anders Brams
committed
chore: release script automatically reads version
1 parent 3511f30 commit 73b65e4

1 file changed

Lines changed: 77 additions & 11 deletions

File tree

scripts/release.py

Lines changed: 77 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,22 @@
77
import sys
88
import tomllib
99
from pathlib import Path
10+
from typing import NamedTuple
1011

1112
ROOT = Path(__file__).resolve().parents[1]
13+
VERSION_PATTERN = re.compile(
14+
r"(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)"
15+
r"(?:(?P<stage>a|b|rc)(?P<stage_number>\d+))?"
16+
)
17+
STAGE_ORDER = {"a": 0, "b": 1, "rc": 2, None: 3}
18+
19+
20+
class Version(NamedTuple):
21+
major: int
22+
minor: int
23+
patch: int
24+
stage: int
25+
stage_number: int
1226

1327

1428
def run(
@@ -30,11 +44,37 @@ def project_version() -> str:
3044
return tomllib.load(file)["project"]["version"]
3145

3246

33-
def require_supported_version(version: str) -> None:
34-
if not re.fullmatch(r"\d+\.\d+\.\d+((a|b|rc)\d+)?", version):
47+
def parse_version(version: str) -> Version | None:
48+
match = VERSION_PATTERN.fullmatch(version)
49+
if not match:
50+
return None
51+
stage = match.group("stage")
52+
stage_number = match.group("stage_number")
53+
return Version(
54+
int(match.group("major")),
55+
int(match.group("minor")),
56+
int(match.group("patch")),
57+
STAGE_ORDER[stage],
58+
int(stage_number or 0),
59+
)
60+
61+
62+
def require_supported_version(version: str) -> Version:
63+
parsed = parse_version(version)
64+
if parsed is None:
3565
print(f"Unsupported release version: {version!r}")
3666
print("Expected X.Y.Z, X.Y.ZaN, X.Y.ZbN, or X.Y.ZrcN.")
3767
sys.exit(1)
68+
return parsed
69+
70+
71+
def require_next_version(current: str, next_version: str) -> None:
72+
if require_supported_version(next_version) <= require_supported_version(current):
73+
print(
74+
f"Next version must be higher than the current version: "
75+
f"{next_version} <= {current}."
76+
)
77+
sys.exit(1)
3878

3979

4080
def require_clean_worktree() -> None:
@@ -47,6 +87,15 @@ def require_clean_worktree() -> None:
4787
sys.exit(1)
4888

4989

90+
def require_branch(branch: str) -> None:
91+
current = run(["git", "branch", "--show-current"], capture=True).stdout.strip()
92+
if current != branch:
93+
print(
94+
f"Release must be run from {branch!r}, but current branch is {current!r}."
95+
)
96+
sys.exit(1)
97+
98+
5099
def require_tag_available(tag: str) -> None:
51100
local = subprocess.run(
52101
["git", "rev-parse", "--verify", "--quiet", f"refs/tags/{tag}"],
@@ -71,28 +120,41 @@ def require_tag_available(tag: str) -> None:
71120
sys.exit(1)
72121

73122

123+
def commit_version_bump(tag: str) -> None:
124+
run(["git", "add", "pyproject.toml", "uv.lock"])
125+
staged = run(["git", "diff", "--cached", "--name-only"], capture=True).stdout
126+
if not staged.strip():
127+
print("Version bump did not change pyproject.toml or uv.lock.")
128+
sys.exit(1)
129+
run(["git", "commit", "-m", f"Release {tag}"])
130+
131+
74132
def main() -> None:
75133
parser = argparse.ArgumentParser(
76134
description="Run local checks and build distributions before releasing."
77135
)
78136
parser.add_argument(
79137
"--version",
80-
help="Expected version. Defaults to the version in pyproject.toml.",
138+
help="Next release version. Prompts when omitted.",
81139
)
82140
args = parser.parse_args()
83141

84-
version = project_version()
85-
require_supported_version(version)
86-
if args.version and args.version != version:
87-
print(
88-
f"Expected version {args.version}, but pyproject.toml contains {version}."
89-
)
142+
current_version = project_version()
143+
require_supported_version(current_version)
144+
print(f"Current version: {current_version}")
145+
146+
next_version = args.version or input("Next version: ").strip()
147+
if not next_version:
148+
print("A next version is required.")
90149
sys.exit(1)
91150

92-
tag = f"v{version}"
151+
require_next_version(current_version, next_version)
152+
tag = f"v{next_version}"
93153

94154
require_clean_worktree()
155+
require_branch("main")
95156
require_tag_available(tag)
157+
run(["uv", "version", next_version, "--no-sync"])
96158

97159
dist = ROOT / "dist"
98160
if dist.exists():
@@ -104,8 +166,12 @@ def main() -> None:
104166
run(["uv", "run", "pytest", "-n", "auto"])
105167
run(["uv", "build"])
106168

169+
commit_version_bump(tag)
170+
run(["git", "push", "origin", "main"])
171+
run(["git", "push", "origin", "HEAD:releases"])
172+
107173
print(f"Release checks passed for {tag}.")
108-
print("Publish with: git push origin HEAD:releases")
174+
print("Release pushed to main and releases.")
109175

110176

111177
if __name__ == "__main__":

0 commit comments

Comments
 (0)