Compare commits

...
Author SHA1 Message Date
timmy 965b24a183 fix: advertise retry delay on context timeout (#40)
CI / lint (pull_request) Successful in 8s
CI / build-frontend (pull_request) Successful in 5s
2026-08-05 15:03:27 +00:00
timmy 9ea8667264 Merge pull request 'feat: Ops: hourly release pulse GITEA_TOKEN authentication regression' (#39) from timmy/37-ops-hourly-release-pulse-gitea-token-authenticat into main
CI / lint (push) Successful in 8s
Release / release-candidate (push) Successful in 4s
CI / build-frontend (push) Successful in 4s
2026-08-05 12:03:03 +00:00
timmy 9cdf9110fb fix: clarify stale release token recovery (#37)
CI / lint (pull_request) Successful in 8s
CI / build-frontend (pull_request) Successful in 5s
2026-08-05 12:02:15 +00:00
timmy 360425d3ca Merge pull request 'feat: Documentation audit for stackchain-dashboard' (#38) from timmy/35-documentation-audit-for-stackchain-dashboard into main
CI / lint (push) Successful in 8s
Release / release-candidate (push) Successful in 4s
CI / build-frontend (push) Successful in 4s
2026-08-05 11:03:17 +00:00
timmy 324e1cab4e docs: document service health checks
CI / lint (pull_request) Successful in 8s
CI / build-frontend (pull_request) Successful in 6s
2026-08-05 11:02:20 +00:00
timmy 16e54b63f2 Merge pull request 'feat: Improve stackchain-dashboard health and reliability' (#36) from timmy/34-improve-stackchain-dashboard-health-and-reliabil into main
CI / lint (push) Successful in 8s
Release / release-candidate (push) Successful in 5s
CI / build-frontend (push) Successful in 5s
2026-08-05 10:03:46 +00:00
timmy 610b659fd4 feat: add Gitea readiness probe
CI / lint (pull_request) Successful in 9s
CI / build-frontend (pull_request) Successful in 5s
2026-08-05 10:02:54 +00:00
7 changed files with 103 additions and 4 deletions

No files matched your search

+5
View File
@@ -28,6 +28,11 @@ Gitea connection directly, request
`user`, `repos`, `issues`, and `pull_requests`. Never commit the token or place
it in a tracked configuration file.
For service monitoring, GET `/healthz` is a liveness check that confirms the
API process is running and does not contact Gitea. GET `/readyz` is the
readiness check: it validates the configured Gitea credentials and returns
HTTP 503 with an error when Gitea is unavailable or authentication fails.
Run the test suite with:
```bash
+25 -1
View File
@@ -1,4 +1,5 @@
import asyncio
import math
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
@@ -30,6 +31,27 @@ def health() -> dict[str, str]:
return {"status": "ok", "service": "stackchain-dashboard"}
@app.get("/readyz")
async def readiness():
"""Return readiness after verifying the configured Gitea connection."""
try:
user = await current_user()
except Exception as exc:
return JSONResponse(
{
"status": "not_ready",
"service": "stackchain-dashboard",
"error": str(exc),
},
status_code=503,
)
return {
"status": "ready",
"service": "stackchain-dashboard",
"gitea_user": user["login"],
}
@app.get("/api/v1/context")
async def context() -> JSONResponse:
try:
@@ -54,7 +76,9 @@ async def context() -> JSONResponse:
{"panel": "gitea", "action": "set GITEA_URL + GITEA_TOKEN", "target": "/root/stackchain-dashboard/.env or systemd env", "priority": "high"},
],
"error": error_message,
})
}, headers={
"Retry-After": str(max(1, math.ceil(CONTEXT_TIMEOUT_SECONDS)))
} if isinstance(e, TimeoutError) else None)
user_model = User(id=user_data["id"], login=user_data["login"], full_name=user_data.get("full_name", ""), email=user_data.get("email", ""))
repo_models = [
Repo(id=r["id"], name=r["name"], full_name=r["full_name"], description=r.get("description", ""), url=r["html_url"], updated_at=r.get("updated_at", ""))
+2
View File
@@ -147,6 +147,8 @@ class GiteaClient:
return json.loads(raw) if raw else None
except error.HTTPError as exc:
detail = exc.read().decode(errors="replace")
if exc.code == 401:
detail += " (authentication rejected; refresh the cron GITEA_TOKEN)"
raise RuntimeError(f"Gitea {method} {path} failed: HTTP {exc.code}: {detail}") from exc
@staticmethod
+1
View File
@@ -27,6 +27,7 @@ async def test_context_returns_fallback_when_gitea_exceeds_deadline(monkeypatch)
response = await main.context()
assert response.status_code == 200
assert response.headers["retry-after"] == "1"
assert b'"repos":[]' in response.body
assert b'"error":"Gitea context request timed out after 0.01s"' in response.body
assert cancelled.is_set()
+36 -3
View File
@@ -1,6 +1,39 @@
from src.main import app, health
import pytest
from src import main
def test_health_endpoint_reports_service_liveness_without_gitea_access():
assert any(getattr(route, "path", None) == "/healthz" for route in app.routes)
assert health() == {"status": "ok", "service": "stackchain-dashboard"}
assert any(getattr(route, "path", None) == "/healthz" for route in main.app.routes)
assert main.health() == {"status": "ok", "service": "stackchain-dashboard"}
@pytest.mark.anyio
async def test_readiness_endpoint_reports_connected_gitea_user(monkeypatch):
async def connected_user():
return {"login": "timmy"}
monkeypatch.setattr(main, "current_user", connected_user)
response = await main.readiness()
assert any(getattr(route, "path", None) == "/readyz" for route in main.app.routes)
assert response == {
"status": "ready",
"service": "stackchain-dashboard",
"gitea_user": "timmy",
}
@pytest.mark.anyio
async def test_readiness_endpoint_returns_503_when_gitea_is_unavailable(monkeypatch):
async def unavailable_user():
raise RuntimeError("connection refused")
monkeypatch.setattr(main, "current_user", unavailable_user)
response = await main.readiness()
assert response.status_code == 503
assert b'"status":"not_ready"' in response.body
assert b'"error":"connection refused"' in response.body
+9
View File
@@ -23,3 +23,12 @@ def test_readme_documents_a_complete_local_quickstart():
assert "uvicorn src.main:app" in text
assert "http://127.0.0.1:8000/api/v1/context" in text
assert "uvicorn==" in REQUIREMENTS.read_text()
def test_readme_documents_liveness_and_gitea_readiness_checks():
text = README.read_text()
assert "`/healthz`" in text
assert "`/readyz`" in text
assert "does not contact Gitea" in text
assert "HTTP 503" in text
+25
View File
@@ -1,4 +1,6 @@
import json
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
import pytest
@@ -6,6 +8,7 @@ import pytest
from src.release_engine import (
CommandResult,
EngineConfig,
GiteaClient,
Issue,
ReleaseEngine,
RunState,
@@ -15,6 +18,28 @@ from src.release_engine import (
)
def test_stale_token_error_explains_how_to_restore_cron_authentication():
class UnauthorizedHandler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(401)
self.end_headers()
self.wfile.write(b'{"message":"user does not exist [uid: 0, name: ]"}')
def log_message(self, format, *args):
pass
server = HTTPServer(("127.0.0.1", 0), UnauthorizedHandler)
thread = threading.Thread(target=server.handle_request)
thread.start()
try:
client = GiteaClient(f"http://127.0.0.1:{server.server_port}", "stale-token")
with pytest.raises(RuntimeError, match="refresh the cron GITEA_TOKEN"):
client.list_open_issues("stackchain/stackchain-dashboard")
finally:
thread.join(timeout=2)
server.server_close()
class FakeClient:
def __init__(self, issues, verified_assignee="timmy", existing_pr=None):
self.issues = issues