Add release helper script
This commit is contained in:
20
README.md
20
README.md
@@ -50,6 +50,26 @@ docker compose up --build
|
|||||||
|
|
||||||
Open: http://127.0.0.1:5000
|
Open: http://127.0.0.1:5000
|
||||||
|
|
||||||
|
## Release (git + docker)
|
||||||
|
|
||||||
|
There is a helper script that:
|
||||||
|
|
||||||
|
- updates `release.py` with the version
|
||||||
|
- commits + pushes to git using the version as the commit message
|
||||||
|
- builds + pushes Docker images tagged with the version (and optionally `latest`)
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python release_tool.py v1.0.2 --image git.alphen.cloud/bramval/rssfeedviewer --also-latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Dry run (prints commands only):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python release_tool.py v1.0.2 --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
## Controls
|
## Controls
|
||||||
|
|
||||||
- Click/tap: next headline
|
- Click/tap: next headline
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ This module exists so the running application and/or deployments can introspect
|
|||||||
which version is currently deployed.
|
which version is currently deployed.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
VERSION = "v1.0.1"
|
VERSION = "v1.0.1"
|
||||||
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