Release v0.1.16 2026-07-18
This commit is contained in:
+42
-11
@@ -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()
|
||||
result = gitea_request(
|
||||
f"/repos/{GITEA_OWNER}/{GITEA_REPO}/releases",
|
||||
token=token,
|
||||
method="POST",
|
||||
body=payload,
|
||||
)
|
||||
path = f"/repos/{GITEA_OWNER}/{GITEA_REPO}/releases"
|
||||
result = None
|
||||
for attempt in range(3):
|
||||
try:
|
||||
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):
|
||||
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