Release v0.1.16 2026-07-18

This commit is contained in:
Warren H
2026-07-18 20:50:51 -04:00
parent ea5d4550a5
commit 5045f17b94
5 changed files with 155 additions and 14 deletions
+1
View File
@@ -10,6 +10,7 @@ vite.config.d.ts
.env .env
.env.* .env.*
!.env.example !.env.example
/git-token
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
/public/basis/ /public/basis/
+10 -2
View File
@@ -97,8 +97,16 @@ The repository target is:
https://git.whoagland.com/phenom/i-want-to-heal-mmo.git https://git.whoagland.com/phenom/i-want-to-heal-mmo.git
``` ```
The Mac publisher is configured with the Gitea release token. `GITEA_TOKEN` can Create `git-token` in the repository root. Paste only the Gitea token into it:
optionally override it for one run. Publish from `main`:
```text
gitea_token_value_goes_here
```
Do not add quotes or a `GITEA_TOKEN=` prefix. The exact `/git-token` path is
Git-ignored. Restrict local file access with `chmod 600 git-token`. The publisher
reads it automatically. `GITEA_TOKEN` remains available as an optional override.
Publish from `main` normally:
```bash ```bash
pnpm publish:gitea -- --message "Describe the update" pnpm publish:gitea -- --message "Describe the update"
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "i-want-to-heal", "name": "i-want-to-heal",
"private": true, "private": true,
"version": "0.1.15", "version": "0.1.16",
"type": "module", "type": "module",
"scripts": { "scripts": {
"predev": "node scripts/sync_basis_transcoder.mjs", "predev": "node scripts/sync_basis_transcoder.mjs",
+42 -11
View File
@@ -11,6 +11,7 @@ import re
import shutil import shutil
import subprocess import subprocess
import sys import sys
import time
import urllib.error import urllib.error
import urllib.parse import urllib.parse
import urllib.request import urllib.request
@@ -21,9 +22,9 @@ from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1] REPO_ROOT = Path(__file__).resolve().parents[1]
ANDROID_ROOT = REPO_ROOT / "android" ANDROID_ROOT = REPO_ROOT / "android"
PACKAGE_JSON = REPO_ROOT / "package.json" PACKAGE_JSON = REPO_ROOT / "package.json"
GITEA_TOKEN_FILE = REPO_ROOT / "git-token"
GITEA_REMOTE = "https://git.whoagland.com/phenom/i-want-to-heal-mmo.git" GITEA_REMOTE = "https://git.whoagland.com/phenom/i-want-to-heal-mmo.git"
GITEA_API = "https://git.whoagland.com/api/v1" GITEA_API = "https://git.whoagland.com/api/v1"
GITEA_TOKEN = "ed2db3fd54546e9658377d0551b3fc3961583f1d"
GITEA_OWNER = "phenom" GITEA_OWNER = "phenom"
GITEA_REPO = "i-want-to-heal-mmo" GITEA_REPO = "i-want-to-heal-mmo"
BRANCH = "main" BRANCH = "main"
@@ -34,6 +35,15 @@ TRUENAS_GITEA_REPO = Path(
SEMVER = re.compile(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$") SEMVER = re.compile(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$")
class GiteaAPIError(SystemExit):
def __init__(self, method: str, path: str, status: int, details: str) -> None:
self.method = method
self.path = path
self.status = status
self.details = details
super().__init__(f"Gitea API {method} {path} failed ({status}): {details}")
def run( def run(
args: list[str], args: list[str],
*, *,
@@ -258,11 +268,13 @@ def ensure_tag(version: str, commit: str, message: str) -> str:
def gitea_token() -> str: def gitea_token() -> str:
token = os.environ.get("GITEA_TOKEN", GITEA_TOKEN).strip() token = os.environ.get("GITEA_TOKEN", "").strip()
if not token and GITEA_TOKEN_FILE.is_file():
token = GITEA_TOKEN_FILE.read_text().strip()
if not token: if not token:
raise SystemExit( raise SystemExit(
"GITEA_TOKEN is required to create the release and upload the APK. " f"Gitea token is required. Paste it into {GITEA_TOKEN_FILE}, "
"Use --skip-release to push source/tag only." "set GITEA_TOKEN, or use --skip-release."
) )
return token return token
@@ -294,7 +306,7 @@ def gitea_request(
details = error.read().decode(errors="replace") details = error.read().decode(errors="replace")
if allow_not_found and error.code == 404: if allow_not_found and error.code == 404:
return None return None
raise SystemExit(f"Gitea API {method} {path} failed ({error.code}): {details}") from error raise GiteaAPIError(method, path, error.code, details) from error
return json.loads(payload) if payload else None return json.loads(payload) if payload else None
@@ -327,12 +339,31 @@ def create_release(tag: str, commit: str, message: str, token: str) -> dict[str,
"prerelease": True, "prerelease": True,
} }
).encode() ).encode()
result = gitea_request( path = f"/repos/{GITEA_OWNER}/{GITEA_REPO}/releases"
f"/repos/{GITEA_OWNER}/{GITEA_REPO}/releases", result = None
token=token, for attempt in range(3):
method="POST", try:
body=payload, result = gitea_request(
) path,
token=token,
method="POST",
body=payload,
)
break
except GiteaAPIError as error:
tag_sync_race = (
error.status == 500
and "UQE_release_n" in error.details
and "23505" in error.details
)
if not tag_sync_race or attempt == 2:
raise
delay = 0.5 * (attempt + 1)
print(
f"Gitea tag sync still settling for {tag}; "
f"retrying release in {delay:g}s."
)
time.sleep(delay)
if not isinstance(result, dict): if not isinstance(result, dict):
raise SystemExit("Gitea returned an invalid release response") raise SystemExit("Gitea returned an invalid release response")
return result return result
+101
View File
@@ -0,0 +1,101 @@
from __future__ import annotations
import os
import unittest
from unittest.mock import MagicMock, patch
from scripts import publish_gitea
class GiteaTokenTests(unittest.TestCase):
def test_reads_ignored_token_file(self) -> None:
token_file = MagicMock()
token_file.is_file.return_value = True
token_file.read_text.return_value = "local-token\n"
with (
patch.dict(os.environ, {}, clear=True),
patch.object(publish_gitea, "GITEA_TOKEN_FILE", token_file),
):
self.assertEqual(publish_gitea.gitea_token(), "local-token")
def test_environment_token_overrides_file(self) -> None:
token_file = MagicMock()
with (
patch.dict(os.environ, {"GITEA_TOKEN": "environment-token"}, clear=True),
patch.object(publish_gitea, "GITEA_TOKEN_FILE", token_file),
):
self.assertEqual(publish_gitea.gitea_token(), "environment-token")
token_file.is_file.assert_not_called()
def test_requires_token(self) -> None:
token_file = MagicMock()
token_file.is_file.return_value = False
with (
patch.dict(os.environ, {}, clear=True),
patch.object(publish_gitea, "GITEA_TOKEN_FILE", token_file),
):
with self.assertRaisesRegex(SystemExit, "Gitea token is required"):
publish_gitea.gitea_token()
class CreateReleaseTests(unittest.TestCase):
def test_retries_postgres_tag_sync_collision(self) -> None:
collision = publish_gitea.GiteaAPIError(
"POST",
"/repos/phenom/i-want-to-heal-mmo/releases",
500,
'duplicate key violates "UQE_release_n" (23505)',
)
created = {"id": 15, "tag_name": "v0.1.15"}
with (
patch.object(publish_gitea, "release_for_tag", return_value=None),
patch.object(
publish_gitea,
"gitea_request",
side_effect=[collision, created],
) as request,
patch.object(publish_gitea.time, "sleep") as sleep,
):
result = publish_gitea.create_release(
"v0.1.15",
"ea5d455",
"Release v0.1.15 2026-07-18",
"token",
)
self.assertEqual(result, created)
self.assertEqual(request.call_count, 2)
sleep.assert_called_once_with(0.5)
def test_does_not_retry_unrelated_api_error(self) -> None:
unauthorized = publish_gitea.GiteaAPIError(
"POST",
"/repos/phenom/i-want-to-heal-mmo/releases",
401,
"unauthorized",
)
with (
patch.object(publish_gitea, "release_for_tag", return_value=None),
patch.object(
publish_gitea,
"gitea_request",
side_effect=unauthorized,
) as request,
patch.object(publish_gitea.time, "sleep") as sleep,
):
with self.assertRaises(publish_gitea.GiteaAPIError):
publish_gitea.create_release(
"v0.1.16",
"commit",
"Release v0.1.16",
"token",
)
request.assert_called_once()
sleep.assert_not_called()
if __name__ == "__main__":
unittest.main()