Phase `02_level_content_and_validation` — final verification **green**. - **Gate:** `bash .agents/validate.sh` → ruff ✅ · pyright 0 errors ✅ · `undercooked-levels validate` OK (2 levels) ✅ · **668 passed** (incl. 10 Playwright e2e) ✅ · coverage **100%** ✅ · repo hygiene ✅ - **Criteria:** `validate` exit 0, one line per level; `list` = `01_first_kitchen`, `02_double_shift` in campaign order; live `:8000` `/healthz` 200, `/api/levels` (+detail) carries both levels with numeric `target_score`/`time_limit` and no grid/station leakage, `/api/config` correct, unknown id → 404, startup log `content loaded: 2 level(s), 4 recipe(s)`; `validate --dir tests/fixtures/levels/_bad/` → exit 1 with named `RECIPE_STATION_MISSING` / `SERVE_REACHABLE` / `TARGET_IMPLAUSIBLE`; A8 lock test proves a YAML-only level reaches loader+API+CLI. - **Defect fixed (pre-existing path, phase-02-triggered):** `undercooked` with unusable content printed the report then a raw traceback and exited `1`. `src/undercooked/__main__.py` now catches `ContentError` → one closing line, exit `2` (like a bad setting); 2 new tests in `tests/unit/test_main.py` (incl. non-content failures still propagate). - **Docs pass (full, project-wide):** README (content gate + exit `2`, config section), AGENTS.md §3, `docs/LEVELS.md` (startup refusal + exact CLI exit codes), `docs/DEPLOYMENT.md` (crash-loop diagnosis bullet); DESIGN/TESTING re-read, consistent. - **Container re-verified (no phase-01 regression):** `podman build -f Containerfile` succeeds; the image ships `levels/` and serves `/healthz` 200, `/api/levels` (2 levels), `/` 200 as non-root. - **Deviation to note:** the phase overview names `tests/fixtures/levels/bad/`; the actual broken-fixture dir is `tests/fixtures/levels/_bad/` (the `_` prefix is what keeps it out of normal discovery). Overview files are read-only to me — README/LEVELS.md document the real path. - No git index/history commands run; all changes left unstaged for the harness. ntfy notification sent.
2.9 KiB
DEPLOYMENT.md — building, running and operating the service
Status: stub written by phase 01 · Filled in by phase 14, which measures the real capacity and writes the reverse-proxy/TLS/ops runbook against a production profile.
Purpose
This document covers everything between "a repository" and "players can reach it": the
multi-stage container build (A19), the single-service compose.yaml, the environment
variables an operator actually sets, how TLS and a reverse proxy terminate the WebSocket
connection, and the runbook for the failure modes that an in-memory server has.
README.md "Container" stays the copy-paste quickstart; this file is the reasoning and
the operational detail.
What is deployable today
One artifact, one process, no database (A6/A7): the image builds the package with
pip install --prefix=/install . in a builder stage and copies the result into a slim
Python 3.12 runtime that runs as the non-root player user (uid 10001), exposes port
8000, and health-checks GET /healthz from inside the container. compose.yaml runs that
one service with restart: unless-stopped. Configuration is environment-only — see the
README table and .env.example; the container sets UNDERCOOKED_HOST=0.0.0.0 and points
UNDERCOOKED_STATIC_DIR / UNDERCOOKED_LEVELS_DIR at the paths copied into the image,
because the source-checkout defaults do not hold under site-packages.
podman build -t undercooked -f Containerfile .
podman run --rm -p 8000:8000 undercooked
podman compose up -d
Rules that hold for every deployment of this project:
- Never set
DEBUGPY=1on a host reachable from outside your machine — the debugger listener binds0.0.0.0(A18, dev-only). - Never commit
.env. It is gitignored, and.env.exampleis the secret-free template; the game has no secrets by design (anonymous, A6/A16). - Everything is in memory: a restart ends every round and erases every scoreboard. That is a documented product decision, not an incident.
- The campaign in the image is checked before the port opens.
create_apploads and validatesUNDERCOOKED_LEVELS_DIRat startup, so an image built around content that breaks a PLAN §8 rule exits2with the named-rule report on stderr instead of serving an empty lobby; withrestart: unless-stoppedthat looks like a crash loop whose logs are the diagnosis.uv run undercooked-levels validatereproduces it from the checkout.
What this document will contain when it is filled in
Reverse-proxy configuration (WebSocket upgrade, Connection: upgrade, idle timeouts
longer than the room's heartbeat), TLS termination and HSTS within the project's
default-src 'self' CSP (A12), resource limits derived from the phase-12 capacity
measurement in BUDGETS.md, log/metric surfaces that never record a room code with player
contents (§11), a rolling-restart procedure that accepts the round it kills, and the
backup story: none, and why.