#!/usr/bin/env python3 """Builds the production images of the backend and the frontend with a version tag. python scripts/release.py 0.0.1 # an exact version python scripts/release.py patch # bump: patch | minor | major (0.0.1 -> 0.0.2) python scripts/release.py patch --platform linux/arm64 # for a server of another architecture python scripts/release.py --dry-run patch # only print what would be done The version is kept in the VERSION file in the repository root (the bump starts from 0.0.0 when there is no such file). Images: fitness_parser_backend: (target `prod`) and fitness_parser_frontend:. """ import argparse import re import subprocess import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent VERSION_FILE = ROOT / "VERSION" VERSION_PATTERN = re.compile(r"^\d+\.\d+\.\d+$") # image name -> (build context, extra docker build arguments) IMAGES = { "fitness_parser_backend": ("backend", ["--target", "prod"]), "fitness_parser_frontend": ("frontend", []), } def current_version() -> str: if not VERSION_FILE.exists(): return "0.0.0" return VERSION_FILE.read_text().strip() def next_version(request: str) -> str: if VERSION_PATTERN.match(request): return request major, minor, patch = (int(part) for part in current_version().split(".")) if request == "major": return f"{major + 1}.0.0" if request == "minor": return f"{major}.{minor + 1}.0" if request == "patch": return f"{major}.{minor}.{patch + 1}" sys.exit(f"Unknown version '{request}': use X.Y.Z, patch, minor or major") def build_command(name: str, version: str, platform: str | None) -> list[str]: context, extra = IMAGES[name] if platform: # buildx can build for another architecture; --load puts the image into local docker command = ["docker", "buildx", "build", "--platform", platform, "--load"] else: command = ["docker", "build"] return [*command, *extra, "-t", f"{name}:{version}", str(ROOT / context)] def main() -> None: parser = argparse.ArgumentParser(description=__doc__.split("\n")[0]) parser.add_argument("version", help="X.Y.Z, or patch / minor / major to bump the current one") parser.add_argument("--platform", help="target platform, e.g. linux/arm64 (default: this machine)") parser.add_argument("--dry-run", action="store_true", help="print the commands, change nothing") args = parser.parse_args() version = next_version(args.version) print(f"Version: {current_version()} -> {version}") commands = [build_command(name, version, args.platform) for name in IMAGES] if args.dry_run: for command in commands: print(" ".join(command)) return for command in commands: print("+", " ".join(command), flush=True) if subprocess.run(command).returncode != 0: sys.exit("Build failed, the VERSION file was not changed") # Written only after both images were built VERSION_FILE.write_text(version + "\n") images = " ".join(f"{name}:{version}" for name in IMAGES) print(f"\nBuilt {images}\nSend to the server:\n" f" docker save {images} | gzip | ssh user@host 'gunzip | docker load'") if __name__ == "__main__": main()