128 lines
5.1 KiB
Python
Executable File
128 lines
5.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""CI wrapper for the production Dashboard ASGI HTTP smoke gate.
|
|
|
|
Creates a synthetic fixture DB, starts the authenticated Streamlit ASGI wrapper,
|
|
waits for HTTP 200, and always attempts process cleanup. Output is intentionally
|
|
status-only.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import signal
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
import urllib.parse
|
|
import urllib.request
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(REPO_ROOT))
|
|
|
|
from scripts.ci_fixture_db import create_ci_fixture_db
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description="Run the Dashboard ASGI HTTP smoke against a synthetic CI fixture DB.")
|
|
parser.add_argument("--host", default="127.0.0.1", help="Loopback host to bind")
|
|
parser.add_argument("--port", type=int, default=18501, help="Deterministic smoke port")
|
|
parser.add_argument("--timeout", type=int, default=45, help="Readiness timeout in seconds")
|
|
parser.add_argument("--page", default="요약", help="Dashboard page query param to request")
|
|
return parser.parse_args()
|
|
|
|
|
|
def wait_for_http(url: str, timeout_seconds: int) -> dict[str, Any]:
|
|
deadline = time.monotonic() + timeout_seconds
|
|
attempts = 0
|
|
last_error = "not_started"
|
|
while time.monotonic() < deadline:
|
|
attempts += 1
|
|
try:
|
|
with urllib.request.urlopen(url, timeout=2) as response:
|
|
if response.status == 200:
|
|
return {"ready": True, "status": response.status, "attempts": attempts}
|
|
last_error = f"HTTP {response.status}"
|
|
except Exception as exc: # noqa: BLE001 - compact readiness diagnostic
|
|
last_error = exc.__class__.__name__
|
|
time.sleep(1)
|
|
return {"ready": False, "attempts": attempts, "last_error": last_error}
|
|
|
|
|
|
def terminate_process(proc: subprocess.Popen[str], timeout: int = 10) -> dict[str, Any]:
|
|
if proc.poll() is not None:
|
|
return {"cleanup": "already_exited", "returncode": proc.returncode}
|
|
try:
|
|
proc.send_signal(signal.SIGTERM)
|
|
proc.wait(timeout=timeout)
|
|
return {"cleanup": "terminated", "returncode": proc.returncode}
|
|
except subprocess.TimeoutExpired:
|
|
proc.kill()
|
|
proc.wait(timeout=timeout)
|
|
return {"cleanup": "killed", "returncode": proc.returncode}
|
|
|
|
|
|
def main() -> int:
|
|
args = parse_args()
|
|
smoke_url = f"http://{args.host}:{args.port}/?page={urllib.parse.quote(args.page)}"
|
|
proc: subprocess.Popen[str] | None = None
|
|
with tempfile.TemporaryDirectory(prefix="jarca-ledger-ci-http-") as tmp:
|
|
tmp_path = Path(tmp)
|
|
fixture_path = tmp_path / "fixture.sqlite3"
|
|
try:
|
|
fixture = create_ci_fixture_db(fixture_path)
|
|
env = os.environ.copy()
|
|
env["JARCA_LEDGER_DB_PATH"] = str(fixture_path)
|
|
env["JARCA_DASHBOARD_ADMIN_ID"] = "a"
|
|
env["JARCA_DASHBOARD_ADMIN_PASSWORD"] = "p"
|
|
env["JARCA_DASHBOARD_SIGNING_SECRET"] = "s" * 32
|
|
env["FORWARDED_ALLOW_IPS"] = "127.0.0.1"
|
|
env["STREAMLIT_BROWSER_GATHER_USAGE_STATS"] = "false"
|
|
cmd = [
|
|
sys.executable,
|
|
"-m",
|
|
"uvicorn",
|
|
"dashboard.server:app",
|
|
"--host",
|
|
args.host,
|
|
"--port",
|
|
str(args.port),
|
|
"--proxy-headers",
|
|
]
|
|
proc = subprocess.Popen(
|
|
cmd,
|
|
cwd=Path(__file__).resolve().parents[1],
|
|
env=env,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
text=True,
|
|
)
|
|
readiness = wait_for_http(smoke_url, args.timeout)
|
|
if not readiness["ready"]:
|
|
cleanup = terminate_process(proc)
|
|
print(json.dumps({"success": False, "gate": "dashboard-http-smoke", "fixture": "synthetic", "readiness": readiness, "cleanup": cleanup}, ensure_ascii=False))
|
|
return 10
|
|
cleanup = terminate_process(proc)
|
|
success = cleanup.get("cleanup") in {"terminated", "already_exited", "killed"}
|
|
print(json.dumps({
|
|
"success": success,
|
|
"gate": "dashboard-http-smoke",
|
|
"fixture": "synthetic",
|
|
"fixture_integrity": fixture.get("integrity_check"),
|
|
"http_status": readiness.get("status"),
|
|
"attempts": readiness.get("attempts"),
|
|
"cleanup": cleanup.get("cleanup"),
|
|
}, ensure_ascii=False, indent=2))
|
|
return 0 if success else 11
|
|
except Exception as exc: # noqa: BLE001 - deterministic wrapper failure
|
|
cleanup = terminate_process(proc) if proc is not None else {"cleanup": "not_started"}
|
|
print(json.dumps({"success": False, "gate": "dashboard-http-smoke", "error_code": "wrapper_error", "error": exc.__class__.__name__, "cleanup": cleanup.get("cleanup")}, ensure_ascii=False))
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|