Add release helper script
This commit is contained in:
151
release_tool.py
Normal file
151
release_tool.py
Normal file
@@ -0,0 +1,151 @@
|
||||
"""Release helper.
|
||||
|
||||
Automates the release flow for this repo:
|
||||
|
||||
1) Updates `release.py` VERSION
|
||||
2) Commits with the version as the commit message
|
||||
3) Pushes to the configured git remote
|
||||
4) Builds a Docker image tagged with the version (and optionally `latest`)
|
||||
5) Pushes the Docker tags to the registry
|
||||
|
||||
Usage:
|
||||
python release_tool.py v1.0.2 --image git.alphen.cloud/bramval/rssfeedviewer --also-latest
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent
|
||||
RELEASE_PY = REPO_ROOT / "release.py"
|
||||
|
||||
|
||||
def run(cmd: list[str], *, cwd: Path = REPO_ROOT, dry_run: bool = False) -> None:
|
||||
printable = " ".join(cmd)
|
||||
print(f"$ {printable}")
|
||||
if dry_run:
|
||||
return
|
||||
subprocess.run(cmd, cwd=str(cwd), check=True)
|
||||
|
||||
|
||||
def get_stdout(cmd: list[str], *, cwd: Path = REPO_ROOT) -> str:
|
||||
out = subprocess.check_output(cmd, cwd=str(cwd))
|
||||
return out.decode("utf-8", errors="replace")
|
||||
|
||||
|
||||
def ensure_git_clean() -> None:
|
||||
status = get_stdout(["git", "status", "--porcelain"]).strip()
|
||||
if status:
|
||||
raise RuntimeError(
|
||||
"Working tree is not clean. Please commit/stash changes first.\n" + status
|
||||
)
|
||||
|
||||
|
||||
def update_release_py(version: str, *, dry_run: bool) -> None:
|
||||
content = (
|
||||
'"""Release metadata.\n\n'
|
||||
"This module exists so the running application and/or deployments can introspect\n"
|
||||
"which version is currently deployed.\n"
|
||||
'"""\n\n'
|
||||
f'VERSION = "{version}"\n'
|
||||
)
|
||||
|
||||
# If the file exists, try to surgically replace VERSION; otherwise create it.
|
||||
if RELEASE_PY.exists():
|
||||
existing = RELEASE_PY.read_text(encoding="utf-8")
|
||||
if re.search(r"^VERSION\s*=\s*\".*\"\s*$", existing, flags=re.MULTILINE):
|
||||
updated = re.sub(
|
||||
r"^VERSION\s*=\s*\".*\"\s*$",
|
||||
f'VERSION = "{version}"',
|
||||
existing,
|
||||
flags=re.MULTILINE,
|
||||
)
|
||||
if dry_run:
|
||||
print(f"[dry-run] Would update {RELEASE_PY.name} VERSION to {version}")
|
||||
else:
|
||||
RELEASE_PY.write_text(updated, encoding="utf-8")
|
||||
return
|
||||
|
||||
if dry_run:
|
||||
print(f"[dry-run] Would write {RELEASE_PY.name} with VERSION={version}")
|
||||
return
|
||||
|
||||
RELEASE_PY.write_text(content, encoding="utf-8")
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
parser = argparse.ArgumentParser(description="Release helper for git + docker")
|
||||
parser.add_argument(
|
||||
"version",
|
||||
help="Version string used as git commit message and docker tag (e.g. v1.0.2)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--image",
|
||||
default="git.alphen.cloud/bramval/rssfeedviewer",
|
||||
help="Docker image repository (no tag), e.g. git.alphen.cloud/bramval/rssfeedviewer",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--also-latest",
|
||||
action="store_true",
|
||||
help="Also tag and push :latest in addition to the version tag",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-git",
|
||||
action="store_true",
|
||||
help="Skip git commit/push steps",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-docker",
|
||||
action="store_true",
|
||||
help="Skip docker build/push steps",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Print commands without executing them",
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
version = args.version.strip()
|
||||
if not version:
|
||||
parser.error("version must be non-empty")
|
||||
|
||||
# In dry-run mode we want to be able to preview the flow even if the working
|
||||
# tree is currently dirty.
|
||||
if not args.skip_git and not args.dry_run:
|
||||
ensure_git_clean()
|
||||
elif not args.skip_git and args.dry_run:
|
||||
print("[dry-run] Skipping clean working-tree check")
|
||||
|
||||
update_release_py(version, dry_run=args.dry_run)
|
||||
|
||||
if not args.skip_git:
|
||||
run(["git", "add", "release.py"], dry_run=args.dry_run)
|
||||
run(["git", "commit", "-m", version], dry_run=args.dry_run)
|
||||
run(["git", "push"], dry_run=args.dry_run)
|
||||
|
||||
if not args.skip_docker:
|
||||
tags: list[str] = [f"{args.image}:{version}"]
|
||||
if args.also_latest:
|
||||
tags.append(f"{args.image}:latest")
|
||||
|
||||
build_cmd = ["docker", "build"]
|
||||
for t in tags:
|
||||
build_cmd += ["-t", t]
|
||||
build_cmd.append(".")
|
||||
|
||||
run(build_cmd, dry_run=args.dry_run)
|
||||
for t in tags:
|
||||
run(["docker", "push", t], dry_run=args.dry_run)
|
||||
|
||||
print("Done.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
Reference in New Issue
Block a user