#!/usr/bin/env python3 """Release helper: git commit/push + docker build/push. Flow: 1) Prompt for version 2) git add -A 3) git commit -m "Version " (skips if nothing to commit) 4) git push -u origin main (ensures origin points to target repo) 5) docker build + push tags: and latest """ from __future__ import annotations import re import shutil import subprocess from dataclasses import dataclass @dataclass(frozen=True) class Config: target_git_url: str = "https://git.alphen.cloud/bramval/PsalmbordOnlineCE.git" docker_image: str = "git.alphen.cloud/bramval/psalmbordonlinece" branch: str = "main" VERSION_RE = re.compile(r"^[0-9A-Za-z][0-9A-Za-z._-]{0,127}$") def run(cmd: list[str], *, check: bool = True) -> subprocess.CompletedProcess[str]: """Run command and stream output; return CompletedProcess.""" print(f"\n$ {' '.join(cmd)}") return subprocess.run(cmd, text=True, check=check) def capture(cmd: list[str]) -> str: """Run command and capture stdout.""" return subprocess.check_output(cmd, text=True).strip() def ensure_tools() -> None: for tool in ("git", "docker"): if shutil.which(tool) is None: raise SystemExit(f"Error: '{tool}' is not available in PATH") def prompt_version() -> str: version = input("Enter version (e.g. 1.2.3): ").strip() if not version: raise SystemExit("Error: version cannot be empty") if not VERSION_RE.match(version): raise SystemExit( "Error: invalid version. Use only letters/numbers and . _ - (max 128 chars)" ) return version def prompt_yes_no(question: str, default_no: bool = True) -> bool: prompt = "(y/N)" if default_no else "(Y/n)" ans = input(f"{question} {prompt}: ").strip().lower() if not ans: return not default_no return ans in ("y", "yes") def ensure_origin(config: Config) -> None: try: origin_url = capture(["git", "remote", "get-url", "origin"]) except subprocess.CalledProcessError: origin_url = "" if not origin_url: print(f"Remote 'origin' not found; adding {config.target_git_url}") run(["git", "remote", "add", "origin", config.target_git_url]) return if origin_url != config.target_git_url: print( f"Remote 'origin' is '{origin_url}' -> updating to '{config.target_git_url}'" ) run(["git", "remote", "set-url", "origin", config.target_git_url]) def main() -> int: config = Config() ensure_tools() version = prompt_version() commit_message = f"Version {version}" # Branch warning try: current_branch = capture(["git", "branch", "--show-current"]) except subprocess.CalledProcessError: current_branch = "" if current_branch and current_branch != config.branch: print( f"Warning: you are on branch '{current_branch}' (expected '{config.branch}')." ) if not prompt_yes_no(f"Continue and push '{config.branch}' anyway?", default_no=True): return 1 ensure_origin(config) print("\n== Git: status ==") run(["git", "status", "-sb"], check=False) if not prompt_yes_no("Proceed with commit+push and docker push?", default_no=True): print("Aborted.") return 1 print("\n== Git: add/commit/push ==") run(["git", "add", "-A"]) # commit may fail if nothing to commit try: run(["git", "commit", "-m", commit_message]) print(f"Committed: {commit_message}") except subprocess.CalledProcessError: print("No changes to commit (or commit failed). Continuing...") run(["git", "push", "-u", "origin", config.branch]) print("\n== Docker: build/tag/push ==") print(f"Docker image: {config.docker_image}") print(f"Tags: {version}, latest") run( [ "docker", "build", "-t", f"{config.docker_image}:{version}", "-t", f"{config.docker_image}:latest", ".", ] ) run(["docker", "push", f"{config.docker_image}:{version}"]) run(["docker", "push", f"{config.docker_image}:latest"]) print("\nDone.") print("If docker push failed due to auth, run: docker login git.alphen.cloud") return 0 if __name__ == "__main__": # Make Ctrl+C exit nicely try: raise SystemExit(main()) except KeyboardInterrupt: print("\nAborted (Ctrl+C).") raise SystemExit(130)