Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5045f17b94 |
@@ -10,6 +10,7 @@ vite.config.d.ts
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
/git-token
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
/public/basis/
|
||||
|
||||
@@ -97,8 +97,16 @@ The repository target is:
|
||||
https://git.whoagland.com/phenom/i-want-to-heal-mmo.git
|
||||
```
|
||||
|
||||
The Mac publisher is configured with the Gitea release token. `GITEA_TOKEN` can
|
||||
optionally override it for one run. Publish from `main`:
|
||||
Create `git-token` in the repository root. Paste only the Gitea token into it:
|
||||
|
||||
```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
|
||||
pnpm publish:gitea -- --message "Describe the update"
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "i-want-to-heal",
|
||||
"private": true,
|
||||
"version": "0.1.15",
|
||||
"version": "0.1.16",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"predev": "node scripts/sync_basis_transcoder.mjs",
|
||||
|
||||
@@ -11,6 +11,7 @@ import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
@@ -21,9 +22,9 @@ from pathlib import Path
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
ANDROID_ROOT = REPO_ROOT / "android"
|
||||
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_API = "https://git.whoagland.com/api/v1"
|
||||
GITEA_TOKEN = "ed2db3fd54546e9658377d0551b3fc3961583f1d"
|
||||
GITEA_OWNER = "phenom"
|
||||
GITEA_REPO = "i-want-to-heal-mmo"
|
||||
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*)$")
|
||||
|
||||
|
||||
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(
|
||||
args: list[str],
|
||||
*,
|
||||
@@ -258,11 +268,13 @@ def ensure_tag(version: str, commit: str, message: str) -> 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:
|
||||
raise SystemExit(
|
||||
"GITEA_TOKEN is required to create the release and upload the APK. "
|
||||
"Use --skip-release to push source/tag only."
|
||||
f"Gitea token is required. Paste it into {GITEA_TOKEN_FILE}, "
|
||||
"set GITEA_TOKEN, or use --skip-release."
|
||||
)
|
||||
return token
|
||||
|
||||
@@ -294,7 +306,7 @@ def gitea_request(
|
||||
details = error.read().decode(errors="replace")
|
||||
if allow_not_found and error.code == 404:
|
||||
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
|
||||
|
||||
|
||||
@@ -327,12 +339,31 @@ def create_release(tag: str, commit: str, message: str, token: str) -> dict[str,
|
||||
"prerelease": True,
|
||||
}
|
||||
).encode()
|
||||
path = f"/repos/{GITEA_OWNER}/{GITEA_REPO}/releases"
|
||||
result = None
|
||||
for attempt in range(3):
|
||||
try:
|
||||
result = gitea_request(
|
||||
f"/repos/{GITEA_OWNER}/{GITEA_REPO}/releases",
|
||||
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):
|
||||
raise SystemExit("Gitea returned an invalid release response")
|
||||
return result
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user