-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrelease.py
More file actions
122 lines (99 loc) · 3.4 KB
/
release.py
File metadata and controls
122 lines (99 loc) · 3.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
from __future__ import annotations
import argparse
import re
import shutil
import subprocess
import sys
import tomllib
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def run(
command: list[str], *, capture: bool = False
) -> subprocess.CompletedProcess[str]:
print("+", " ".join(command))
return subprocess.run(
command,
cwd=ROOT,
check=True,
text=True,
stdout=subprocess.PIPE if capture else None,
stderr=subprocess.PIPE if capture else None,
)
def project_version() -> str:
with (ROOT / "pyproject.toml").open("rb") as file:
return tomllib.load(file)["project"]["version"]
def require_supported_version(version: str) -> None:
if not re.fullmatch(r"\d+\.\d+\.\d+((a|b|rc)\d+)?", version):
print(f"Unsupported release version: {version!r}")
print("Expected X.Y.Z, X.Y.ZaN, X.Y.ZbN, or X.Y.ZrcN.")
sys.exit(1)
def require_clean_worktree() -> None:
status = run(["git", "status", "--porcelain"], capture=True).stdout.strip()
if status:
print(
"Release requires a clean git worktree. Commit or stash these changes first:"
)
print(status)
sys.exit(1)
def require_tag_available(tag: str) -> None:
local = subprocess.run(
["git", "rev-parse", "--verify", "--quiet", f"refs/tags/{tag}"],
cwd=ROOT,
text=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
if local.returncode == 0:
print(f"Tag {tag} already exists locally.")
sys.exit(1)
remote = subprocess.run(
["git", "ls-remote", "--exit-code", "--tags", "origin", f"refs/tags/{tag}"],
cwd=ROOT,
text=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
if remote.returncode == 0:
print(f"Tag {tag} already exists on origin.")
sys.exit(1)
def main() -> None:
parser = argparse.ArgumentParser(
description="Prepare and optionally publish from the releases branch."
)
parser.add_argument(
"--version",
help="Expected version. Defaults to the version in pyproject.toml.",
)
parser.add_argument(
"--push-release-branch",
action="store_true",
help="Push the current commit to origin/releases after release checks pass.",
)
args = parser.parse_args()
version = project_version()
require_supported_version(version)
if args.version and args.version != version:
print(
f"Expected version {args.version}, but pyproject.toml contains {version}."
)
sys.exit(1)
tag = f"v{version}"
require_clean_worktree()
require_tag_available(tag)
dist = ROOT / "dist"
if dist.exists():
shutil.rmtree(dist)
run(["uv", "run", "ruff", "check", "--select", "I", "."])
run(["uv", "run", "ruff", "format", "--check", "."])
run(["uv", "run", "basedpyright", "-p", "pyproject.toml"])
run(["uv", "run", "pytest", "-n", "auto"])
run(["uv", "build"])
if args.push_release_branch:
run(["git", "push", "origin", "HEAD:releases"])
print("Pushed current commit to origin/releases.")
print("GitHub Actions will create the release tag and publish after approval.")
else:
print(f"Release checks passed for {tag}.")
print("Publish with: git push origin HEAD:releases")
if __name__ == "__main__":
main()