gitea: fix LFS round-trip post-restart wait (stale on 28.0.0)
continuous-integration/drone/push Build is failing

test_lfs_roundtrip's post-restart poll timed out on gitea 28.0.0 (PR #10 run #31):
_api() caught only HTTPError, so a single urlopen(timeout=20) socket timeout inside
the poll raised and aborted the poll itself instead of being retried; the poll also
hard-coded a 120s deadline + a 20s request timeout, contradicting the recipe's
declared HTTP_TIMEOUT=600 (tests/STYLE.md §5-sized for the warm custom tier; the
28.0.0 boot runs a longer migration window than 1.27.3).

Fix, assertion untouched (§3):
- _api() is never-raising like lifecycle.http_fetch: transient URLError/socket
  timeout/OSError -> (0, {}) so the bounded poll simply retries (each request still
  bounded at 20s; the poll's deadline is what bounds the wait).
- poll deadline derives from recipe_meta.HTTP_TIMEOUT (600) instead of a hard-coded
  120, probing the same /api/v1/version path recipe_meta.READY_PROBE declares.

Evidence: gitea 28.0.0 dev deploy converged + served /api/healthz and /version
(gitea-upgrade-2026-10-02.md 2b); the RED run #31 failed only in this poll while
install/upgrade/backup/restore and every other custom test passed.
This commit is contained in:
2026-10-05 16:45:10 +00:00
parent f675941c34
commit 2a7cdcdee4
+23 -9
View File
@@ -23,6 +23,7 @@ import shutil
import subprocess
import sys
import tempfile
import time
import urllib.error
import urllib.request
@@ -45,7 +46,15 @@ def _lfs_available() -> bool:
return os.path.exists(os.path.join(abra_dir, "recipes", "gitea", _LFS_OVERLAY))
def _api(domain, path, method="GET", body=None, user="", password=""):
API_TIMEOUT = 20
def _api(domain, path, method="GET", body=None, user="", password="", timeout=API_TIMEOUT):
"""One API call → (status, JSON). Never raises: transient errors (connection refused /
dropped mid-restart, DNS, socket timeout) return (0, {}) so a caller's bounded poll can
simply retry, mirroring lifecycle.http_fetch. Only HTTPError was caught before; a socket
timeout on a slow post-restart boot (gitea 28.0.0 migration window, PR #10 run #31) escaped
as TimeoutError and aborted the restart poll itself."""
data = json.dumps(body).encode() if body is not None else None
auth = base64.b64encode(f"{user}:{password}".encode()).decode()
headers = {"Authorization": f"Basic {auth}"}
@@ -55,7 +64,7 @@ def _api(domain, path, method="GET", body=None, user="", password=""):
f"https://{domain}/api/v1{path}", data=data, headers=headers, method=method
)
try:
with urllib.request.urlopen(req, timeout=20, context=_CTX) as r:
with urllib.request.urlopen(req, timeout=timeout, context=_CTX) as r:
raw = r.read()
return r.status, (json.loads(raw) if raw else {})
except urllib.error.HTTPError as e:
@@ -64,6 +73,8 @@ def _api(domain, path, method="GET", body=None, user="", password=""):
return e.code, json.loads(raw)
except (ValueError, json.JSONDecodeError):
return e.code, {}
except (urllib.error.URLError, TimeoutError, OSError): # TimeoutError ⊃ socket.timeout (UP041)
return 0, {}
def _run_git(args, cwd, env=None):
@@ -164,6 +175,8 @@ def test_lfs_roundtrip(live_app):
finally:
shutil.rmtree(fresh_dir, ignore_errors=True)
import recipe_meta # noqa: E402 — per-recipe declared timeouts (tests/STYLE.md §5)
# 7. JWT-secret stability: restart gitea + assert LFS_JWT_SECRET unchanged.
# Read the current secret from inside the container's rendered app.ini.
current_jwt = lifecycle.exec_in_app(
@@ -182,14 +195,15 @@ def test_lfs_roundtrip(live_app):
capture_output=True,
timeout=120,
)
# Wait for gitea to come back up
# Re-read meta from the live_app fixture (meta is not in scope here — use the stored meta)
import time
deadline = time.time() + 120
# Wait for gitea to come back up. Window + probe are DERIVED from the recipe's declared
# readiness (recipe_meta.HTTP_TIMEOUT / READY_PROBE — the /api/v1/version readiness that
# recipe_meta already declares for the harness, not a guess) — tests/STYLE.md §5.
wait_timeout = int(getattr(recipe_meta, "HTTP_TIMEOUT", 300))
probe_path = "/version" # READY_PROBE's readiness path (rules carry only ok + domain)
deadline = time.time() + wait_timeout
status2 = 0
while time.time() < deadline:
status2, _ = _api(live_app, "/version", user=user, password=password)
status2, _ = _api(live_app, probe_path, user=user, password=password)
if status2 == 200:
break
time.sleep(5)