#!/usr/bin/env python3 from __future__ import annotations import json import os import re import shutil import subprocess import sys import urllib.error import urllib.parse import urllib.request from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[1] GRADLE_FILE = REPO_ROOT / "android" / "app" / "build.gradle" JAVA_HOME = "/Applications/Android Studio.app/Contents/jbr/Contents/Home" API_BASE_URL = "https://iwanttoheal.phenomrom.com" GITEA_URL = "https://git.whoagland.com" GITEA_OWNER = "phenom" GITEA_REPO = "i-want-to-heal" GITEA_TOKEN = "ed2db3fd54546e9658377d0551b3fc3961583f1d" BRANCH = "main" TRUENAS_PATH = Path("/mnt/usbssds/apps/iwanttoheal/app") def run(args: list[str], *, cwd: Path, env: dict[str, str] | None = None) -> None: print(f"$ {' '.join(args)}", flush=True) subprocess.run(args, cwd=cwd, env=env, check=True) def prompt_version() -> str: current = GRADLE_FILE.read_text() match = re.search(r'versionName\s+"([^"]+)"', current) current_version = match.group(1) if match else "" suffix = f" [{current_version}]" if current_version else "" version = input(f"Version{suffix}: ").strip() or current_version if not re.fullmatch(r"\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?", version): raise SystemExit("Version must look like 1.1.2") return version def update_gradle_version(version: str) -> None: text = GRADLE_FILE.read_text() code_match = re.search(r"versionCode\s+(\d+)", text) if not code_match: raise SystemExit(f"Could not find versionCode in {GRADLE_FILE}") next_code = int(code_match.group(1)) + 1 text = re.sub(r"versionCode\s+\d+", f"versionCode {next_code}", text, count=1) text = re.sub(r'versionName\s+"[^"]+"', f'versionName "{version}"', text, count=1) GRADLE_FILE.write_text(text) print(f"Android version: {version}, versionCode: {next_code}") def build_apk(version: str) -> Path: update_gradle_version(version) env = os.environ.copy() env["JAVA_HOME"] = JAVA_HOME env["PATH"] = f"{JAVA_HOME}/bin:{env.get('PATH', '')}" env["VITE_API_BASE_URL"] = API_BASE_URL run(["npm", "run", "android:sync"], cwd=REPO_ROOT, env=env) run(["./gradlew", "clean", "assembleDebug"], cwd=REPO_ROOT / "android", env=env) source = REPO_ROOT / "android" / "app" / "build" / "outputs" / "apk" / "debug" / "app-debug.apk" apk = REPO_ROOT / f"IWantToHeal-Thor-v{version}.apk" shutil.copy2(source, apk) print(f"APK: {apk}") return apk def commit_and_push(version: str) -> None: run(["git", "add", "."], cwd=REPO_ROOT) run(["git", "commit", "-m", f"Android build v{version}"], cwd=REPO_ROOT) run(["git", "push", "origin", BRANCH], cwd=REPO_ROOT) def request_json(method: str, path: str, token: str, data: dict | None = None) -> dict: body = json.dumps(data).encode("utf-8") if data is not None else None headers = {"Authorization": f"token {token}", "Content-Type": "application/json"} req = urllib.request.Request(GITEA_URL + path, data=body, headers=headers, method=method) try: with urllib.request.urlopen(req) as resp: return json.loads(resp.read().decode("utf-8")) except urllib.error.HTTPError as exc: detail = exc.read().decode("utf-8", "replace") raise SystemExit(f"Gitea {method} failed ({exc.code}): {detail}") from exc def upload_asset(path: str, token: str, apk: Path) -> dict: boundary = "----iwanttoheal-release-boundary" body = ( f"--{boundary}\r\n" f'Content-Disposition: form-data; name="attachment"; filename="{apk.name}"\r\n' "Content-Type: application/vnd.android.package-archive\r\n\r\n" ).encode("utf-8") body += apk.read_bytes() body += f"\r\n--{boundary}--\r\n".encode("utf-8") headers = { "Authorization": f"token {token}", "Content-Type": f"multipart/form-data; boundary={boundary}", } req = urllib.request.Request(GITEA_URL + path, data=body, headers=headers, method="POST") try: with urllib.request.urlopen(req) as resp: return json.loads(resp.read().decode("utf-8")) except urllib.error.HTTPError as exc: detail = exc.read().decode("utf-8", "replace") raise SystemExit(f"Gitea asset upload failed ({exc.code}): {detail}") from exc def create_gitea_release(version: str, apk: Path) -> None: token = GITEA_TOKEN.strip() if not token or token == "PASTE_YOUR_GITEA_TOKEN_HERE": raise SystemExit("Set GITEA_TOKEN near top of scripts/release_game_cli.py") repo_path = f"/api/v1/repos/{GITEA_OWNER}/{GITEA_REPO}" release = request_json( "POST", repo_path + "/releases", token, { "tag_name": f"v{version}", "target_commitish": BRANCH, "name": f"v{version}", "body": f"I Want to Heal Android build v{version}", "draft": False, "prerelease": False, }, ) release_id = release.get("id") if not release_id: raise SystemExit(f"Gitea release missing id: {release}") asset_name = urllib.parse.quote(apk.name) upload_asset(f"{repo_path}/releases/{release_id}/assets?name={asset_name}", token, apk) print(f"Gitea release uploaded: v{version}") def update_truenas() -> None: if TRUENAS_PATH.exists(): run(["git", "pull"], cwd=TRUENAS_PATH) print("Restart app in TrueNAS UI.") return print("TrueNAS path not mounted here. Run on TrueNAS:") print(f"cd {TRUENAS_PATH}") print("git pull") print("Then restart app in TrueNAS UI.") def main() -> int: os.chdir(REPO_ROOT) version = prompt_version() apk = build_apk(version) commit_and_push(version) create_gitea_release(version, apk) update_truenas() print("Done.") return 0 if __name__ == "__main__": raise SystemExit(main())