#!/usr/bin/env python3 """Build, test, commit, and publish the game to its Gitea repository.""" from __future__ import annotations import argparse import shutil import subprocess import sys from datetime import datetime from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[1] GITEA_REMOTE = "https://git.whoagland.com/phenom/i-want-to-heal-mmo.git" BRANCH = "main" TRUENAS_PATH = Path("/mnt/usbssds/apps/iwanttoheal/app") def run(args: list[str], *, cwd: Path = REPO_ROOT) -> None: print(f"$ {' '.join(args)}", flush=True) subprocess.run(args, cwd=cwd, check=True) def capture(args: list[str], *, cwd: Path = REPO_ROOT) -> str: return subprocess.run( args, cwd=cwd, check=True, capture_output=True, text=True, ).stdout.strip() def parse_args(argv: list[str]) -> argparse.Namespace: default_message = f"Update 3D game {datetime.now():%Y-%m-%d %H:%M}" parser = argparse.ArgumentParser( description="Build, test, and push I Want to Heal 3D MMO to Gitea." ) parser.add_argument( "-m", "--message", default=default_message, help=f"Git commit message (default: {default_message!r}).", ) parser.add_argument( "--skip-checks", action="store_true", help="Skip dependency, build, and test checks before publishing.", ) parser.add_argument( "--skip-truenas", action="store_true", help="Do not pull the new commit into a locally mounted TrueNAS app directory.", ) parser.add_argument( "--dry-run", action="store_true", help="Run checks and validate Git configuration without staging, committing, pushing, or pulling.", ) return parser.parse_args(argv) def package_manager() -> list[str]: pnpm = shutil.which("pnpm") if pnpm: return [pnpm] corepack = shutil.which("corepack") if corepack: return [corepack, "pnpm"] raise SystemExit("pnpm or corepack is required") def run_checks() -> None: pnpm = package_manager() run([*pnpm, "install", "--frozen-lockfile"]) run([*pnpm, "run", "build"]) run([*pnpm, "test"]) def normalized_remote(url: str) -> str: return url.rstrip("/").removesuffix(".git") def ensure_git_target(*, dry_run: bool) -> None: try: origin = capture(["git", "remote", "get-url", "origin"]) except subprocess.CalledProcessError: if dry_run: raise SystemExit(f"Missing origin remote; expected {GITEA_REMOTE}") run(["git", "remote", "add", "origin", GITEA_REMOTE]) origin = GITEA_REMOTE if normalized_remote(origin) != normalized_remote(GITEA_REMOTE): raise SystemExit( f"Refusing to publish to unexpected origin {origin!r}; expected {GITEA_REMOTE!r}" ) branch = capture(["git", "branch", "--show-current"]) if branch != BRANCH: raise SystemExit( f"Refusing to publish branch {branch!r}; switch to {BRANCH!r} first" ) def has_staged_changes() -> bool: result = subprocess.run( ["git", "diff", "--cached", "--quiet", "--exit-code"], cwd=REPO_ROOT, check=False, ) if result.returncode not in (0, 1): raise SystemExit("Could not inspect staged Git changes") return result.returncode == 1 def commit_and_push(message: str) -> None: run(["git", "add", "--all"]) if has_staged_changes(): run(["git", "commit", "-m", message]) else: print("No changes to commit.") run(["git", "push", "--set-upstream", "origin", BRANCH]) def update_truenas() -> None: if (TRUENAS_PATH / ".git").is_dir(): origin = capture(["git", "remote", "get-url", "origin"], cwd=TRUENAS_PATH) if normalized_remote(origin) != normalized_remote(GITEA_REMOTE): raise SystemExit( "Refusing to update TrueNAS from its old repository origin " f"{origin!r}. Replace that checkout with {GITEA_REMOTE!r} first." ) run(["git", "pull", "--ff-only", "origin", BRANCH], cwd=TRUENAS_PATH) print("TrueNAS source updated. Restart the iwanttoheal app in the TrueNAS UI.") return print("New TrueNAS app clone is not mounted on this machine.") print(f"Move or remove the old app directory at {TRUENAS_PATH}, then run:") print(f" git clone {GITEA_REMOTE} {TRUENAS_PATH}") print("For later releases:") print(f" git -C {TRUENAS_PATH} pull --ff-only origin {BRANCH}") print("Then restart the iwanttoheal app in the TrueNAS UI.") def main(argv: list[str] | None = None) -> int: args = parse_args(sys.argv[1:] if argv is None else argv) ensure_git_target(dry_run=args.dry_run) if not args.skip_checks: run_checks() if args.dry_run: print("Dry run complete. No Git or TrueNAS state changed.") return 0 commit_and_push(args.message) if not args.skip_truenas: update_truenas() print("Publish complete.") return 0 if __name__ == "__main__": raise SystemExit(main())