Release 1.5

This commit is contained in:
2026-01-25 17:14:18 +01:00
parent 78f0f379fc
commit 860679d119
3 changed files with 212 additions and 45 deletions

136
scripts/release.py Normal file
View File

@@ -0,0 +1,136 @@
#!/usr/bin/env python3
"""Release helper.
What it does (in order):
1) Ask/provide a commit message and version.
2) Commit & push to the `openslide` git remote.
3) Build + push Docker image tags:
- git.alphen.cloud/bramval/openslide:<version>
- git.alphen.cloud/bramval/openslide:latest
Usage examples:
python scripts/release.py --version 1.2.3 --message "Release 1.2.3"
python scripts/release.py # interactive prompts
Notes:
- Assumes you are already authenticated for git + the Docker registry (docker login).
"""
from __future__ import annotations
import argparse
import re
import subprocess
import sys
from dataclasses import dataclass
from typing import Iterable, Sequence
DEFAULT_GIT_REMOTE = "openslide"
DEFAULT_IMAGE = "git.alphen.cloud/bramval/openslide"
@dataclass(frozen=True)
class ReleaseInfo:
version: str
message: str
_DOCKER_TAG_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
def _run(cmd: Sequence[str], *, dry_run: bool = False) -> None:
printable = " ".join(cmd)
print(f"> {printable}")
if dry_run:
return
subprocess.run(cmd, check=True)
def _capture(cmd: Sequence[str]) -> str:
return subprocess.check_output(cmd, text=True).strip()
def _require_tool(name: str) -> None:
try:
subprocess.run([name, "--version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False)
except FileNotFoundError as e:
raise SystemExit(f"Required tool not found in PATH: {name}") from e
def _validate_version(tag: str) -> str:
tag = tag.strip()
if not tag:
raise ValueError("Version may not be empty")
if not _DOCKER_TAG_RE.match(tag):
raise ValueError(
"Invalid docker tag for version. Use only letters, digits, '.', '_' or '-'. "
"(1..128 chars, must start with [A-Za-z0-9])"
)
return tag
def get_release_info(*, version: str | None, message: str | None) -> ReleaseInfo:
"""Collect commit message + version before doing the rest."""
if version is None:
version = input("Version tag (e.g. 1.2.3): ").strip()
version = _validate_version(version)
if message is None:
message = input(f"Commit message [Release {version}]: ").strip() or f"Release {version}"
return ReleaseInfo(version=version, message=message)
def git_commit_and_push(*, remote: str, message: str, dry_run: bool = False) -> None:
# Stage all changes
_run(["git", "add", "-A"], dry_run=dry_run)
# Only commit if there is something to commit
porcelain = _capture(["git", "status", "--porcelain"]) # empty => clean
if porcelain:
_run(["git", "commit", "-m", message], dry_run=dry_run)
else:
print("No working tree changes detected; skipping git commit.")
branch = _capture(["git", "rev-parse", "--abbrev-ref", "HEAD"])
_run(["git", "push", remote, branch], dry_run=dry_run)
def docker_build_and_push(*, image: str, version: str, dry_run: bool = False) -> None:
version_tag = f"{image}:{version}"
latest_tag = f"{image}:latest"
_run(["docker", "build", "-t", version_tag, "-t", latest_tag, "."], dry_run=dry_run)
_run(["docker", "push", version_tag], dry_run=dry_run)
_run(["docker", "push", latest_tag], dry_run=dry_run)
def main(argv: Iterable[str]) -> int:
parser = argparse.ArgumentParser(description="Commit + push + docker publish helper.")
parser.add_argument("--version", "-v", help="Docker version tag (e.g. 1.2.3)")
parser.add_argument("--message", "-m", help="Git commit message")
parser.add_argument("--remote", default=DEFAULT_GIT_REMOTE, help=f"Git remote to push to (default: {DEFAULT_GIT_REMOTE})")
parser.add_argument("--image", default=DEFAULT_IMAGE, help=f"Docker image name (default: {DEFAULT_IMAGE})")
parser.add_argument("--dry-run", action="store_true", help="Print commands without executing")
args = parser.parse_args(list(argv))
_require_tool("git")
_require_tool("docker")
info = get_release_info(version=args.version, message=args.message)
print(f"\nReleasing version: {info.version}")
print(f"Commit message: {info.message}")
print(f"Git remote: {args.remote}")
print(f"Docker image: {args.image}\n")
git_commit_and_push(remote=args.remote, message=info.message, dry_run=args.dry_run)
docker_build_and_push(image=args.image, version=info.version, dry_run=args.dry_run)
print("\nDone.")
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))