From de2f86e10118c15bcf2c21ba93eaf2ae2ab7fd6d Mon Sep 17 00:00:00 2001 From: Warren H Date: Sun, 5 Jul 2026 23:31:04 -0400 Subject: [PATCH] Android build v1.1.26 --- IWantToHeal-Thor-v1.1.26.apk | Bin 9854724 -> 9854724 bytes android/app/build.gradle | 2 +- docs/push-updates.md | 6 + .../release_game.cpython-314 2.pyc | Bin 0 -> 34398 bytes scripts/release_game.py | 59 ++++++--- scripts/release_game_cli.py | 123 ++++++++++++++---- 6 files changed, 149 insertions(+), 41 deletions(-) create mode 100644 scripts/__pycache__/release_game.cpython-314 2.pyc diff --git a/IWantToHeal-Thor-v1.1.26.apk b/IWantToHeal-Thor-v1.1.26.apk index f38f7266b375e3e4b3fca7cd933e3a32b855e09f..761b0bec6e9dea0c8d6dd490397c1d4c45cac81e 100644 GIT binary patch delta 2735 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 W{onN>(%K4)|NlnFXW9z}@Bar1dO%J9 diff --git a/android/app/build.gradle b/android/app/build.gradle index f29bcde..495f50e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -7,7 +7,7 @@ android { applicationId "com.warren.iwanttoheal" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 105 + versionCode 106 versionName "1.1.26" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { diff --git a/docs/push-updates.md b/docs/push-updates.md index 789199b..9018eb4 100644 --- a/docs/push-updates.md +++ b/docs/push-updates.md @@ -96,6 +96,12 @@ curl -sS -X POST "$GITEA_URL/api/v1/repos/$GITEA_OWNER/$GITEA_REPO/releases/$REL -F "attachment=@$APK" ``` +If the CLI release fails after creating the Gitea release but before uploading the APK asset, retry only the release upload: + +```sh +npm run release:cli -- --release-only 1.1.26 +``` + ## Step 5: Update TrueNAS ```sh diff --git a/scripts/__pycache__/release_game.cpython-314 2.pyc b/scripts/__pycache__/release_game.cpython-314 2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5a725c8351093f0ca9a0b6784107318cc830d20b GIT binary patch literal 34398 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 tuple[int, bytes]: + curl = shutil.which("curl") + if curl is None: + raise ReleaseError("curl is required for Gitea asset uploads") + + url = config.gitea_url.rstrip("/") + path + proc = subprocess.run( + [ + curl, + "--silent", + "--show-error", + "--retry", + "3", + "--retry-all-errors", + "--retry-delay", + "2", + "--request", + "POST", + url, + "--header", + f"Authorization: token {config.token}", + "--form", + f"attachment=@{apk_path}", + "--write-out", + "\n%{http_code}", + ], + check=False, + capture_output=True, + ) + if proc.returncode != 0: + detail = (proc.stderr or proc.stdout).decode("utf-8", "replace").strip() + raise ReleaseError(f"Gitea asset upload failed: curl exited {proc.returncode}: {detail}") + try: + body, status_text = proc.stdout.rsplit(b"\n", 1) + return int(status_text), body + except ValueError as exc: + output = proc.stdout.decode("utf-8", "replace") + raise ReleaseError(f"Gitea asset upload returned malformed curl output: {output}") from exc def parse_json_response(status: int, data: bytes, action: str) -> dict: @@ -211,26 +254,12 @@ def upload_release_asset(config: ReleaseConfig, log: Callable[[str], None]) -> N raise ReleaseError(f"APK not found for release upload: {apk_path}") release_id = find_or_create_release(config, log) - boundary = f"----iwanttoheal{int(time.time() * 1000)}" - header = ( - f"--{boundary}\r\n" - f'Content-Disposition: form-data; name="attachment"; filename="{apk_path.name}"\r\n' - "Content-Type: application/vnd.android.package-archive\r\n\r\n" - ).encode("utf-8") - footer = f"\r\n--{boundary}--\r\n".encode("utf-8") - body = header + apk_path.read_bytes() + footer asset_name = urllib.parse.quote(apk_path.name) path = ( f"/api/v1/repos/{config.gitea_owner}/{config.gitea_repo}" f"/releases/{release_id}/assets?name={asset_name}" ) - status, data = gitea_request( - config, - "POST", - path, - body=body, - content_type=f"multipart/form-data; boundary={boundary}", - ) + status, data = gitea_upload_asset(config, path, apk_path) if status == 409: raise ReleaseError(f"Release asset already exists: {apk_path.name}") parse_json_response(status, data, "Upload release asset") diff --git a/scripts/release_game_cli.py b/scripts/release_game_cli.py index 610c4a8..584ac76 100755 --- a/scripts/release_game_cli.py +++ b/scripts/release_game_cli.py @@ -68,9 +68,16 @@ def parse_args(argv: list[str]) -> argparse.Namespace: action="store_true", help="Skip the RESET confirmation when used with --reset-player-data.", ) + parser.add_argument( + "--release-only", + metavar="VERSION", + help="Create or reuse the Gitea release and upload an existing IWantToHeal-Thor-vVERSION.apk, without rebuilding.", + ) args = parser.parse_args(argv) if args.yes_reset and not args.reset_player_data: parser.error("--yes-reset requires --reset-player-data") + if args.release_only and args.reset_player_data: + parser.error("--release-only cannot be combined with --reset-player-data") return args @@ -131,44 +138,90 @@ def request_json(method: str, path: str, token: str, data: dict | None = None) - except urllib.error.HTTPError as exc: detail = exc.read().decode("utf-8", "replace") raise SystemExit(f"Gitea {method} failed ({exc.code}): {detail}") from exc + except urllib.error.URLError as exc: + raise SystemExit(f"Gitea {method} failed: {exc.reason}") 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") + url = GITEA_URL + path + curl = shutil.which("curl") + if curl is None: + raise SystemExit("curl is required for Gitea asset uploads") + + proc = subprocess.run( + [ + curl, + "--silent", + "--show-error", + "--retry", + "3", + "--retry-all-errors", + "--retry-delay", + "2", + "--request", + "POST", + url, + "--header", + f"Authorization: token {token}", + "--form", + f"attachment=@{apk}", + "--write-out", + "\n%{http_code}", + ], + check=False, + capture_output=True, + text=True, + ) + if proc.returncode != 0: + detail = proc.stderr.strip() or proc.stdout.strip() + raise SystemExit(f"Gitea asset upload failed: curl exited {proc.returncode}: {detail}") + + try: + body, status_text = proc.stdout.rsplit("\n", 1) + status = int(status_text) + except ValueError as exc: + raise SystemExit(f"Gitea asset upload returned malformed curl output: {proc.stdout}") from exc + + if status >= 400: + raise SystemExit(f"Gitea asset upload failed ({status}): {body}") + + try: + parsed = json.loads(body or "{}") + except json.JSONDecodeError as exc: + raise SystemExit(f"Gitea asset upload returned invalid JSON: {body}") from exc + if not isinstance(parsed, dict): + raise SystemExit(f"Gitea asset upload returned unexpected JSON: {parsed}") + return parsed + + +def find_or_create_release(version: str, token: str) -> int: + repo_path = f"/api/v1/repos/{GITEA_OWNER}/{GITEA_REPO}" + tag = f"v{version}" + tag_path = repo_path + "/releases/tags/" + urllib.parse.quote(tag, safe="") + req = urllib.request.Request(GITEA_URL + tag_path, headers={"Authorization": f"token {token}"}, method="GET") try: with urllib.request.urlopen(req) as resp: - return json.loads(resp.read().decode("utf-8")) + release = json.loads(resp.read().decode("utf-8")) + release_id = release.get("id") + if not release_id: + raise SystemExit(f"Gitea release missing id: {release}") + print(f"Gitea release exists: {tag} (id {release_id})") + return int(release_id) 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 + if exc.code != 404: + detail = exc.read().decode("utf-8", "replace") + raise SystemExit(f"Gitea release lookup failed ({exc.code}): {detail}") from exc + except urllib.error.URLError as exc: + raise SystemExit(f"Gitea release lookup failed: {exc.reason}") 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}", + "tag_name": tag, "target_commitish": BRANCH, - "name": f"v{version}", + "name": tag, "body": f"I Want to Heal Android build v{version}", "draft": False, "prerelease": False, @@ -177,7 +230,17 @@ def create_gitea_release(version: str, apk: Path) -> None: release_id = release.get("id") if not release_id: raise SystemExit(f"Gitea release missing id: {release}") + print(f"Gitea release created: {tag} (id {release_id})") + return int(release_id) + +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_id = find_or_create_release(version, token) 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}") @@ -289,6 +352,16 @@ def reset_player_data(database_path: Path, *, assume_yes: bool) -> None: def main(argv: list[str] | None = None) -> int: args = parse_args(sys.argv[1:] if argv is None else argv) os.chdir(REPO_ROOT) + if args.release_only: + version = args.release_only.strip() + if not re.fullmatch(r"\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?", version): + raise SystemExit("Version must look like 1.1.2") + apk = REPO_ROOT / f"IWantToHeal-Thor-v{version}.apk" + if not apk.exists(): + raise SystemExit(f"APK not found: {apk}") + create_gitea_release(version, apk) + return 0 + version = prompt_version() apk = build_apk(version) commit_and_push(version)