Files
ducoterra 3426264ffb phase: 02_level_content_and_validation
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.
2026-10-06 11:03:28 -04:00

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=1 on a host reachable from outside your machine — the debugger listener binds 0.0.0.0 (A18, dev-only).
  • Never commit .env. It is gitignored, and .env.example is 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_app loads and validates UNDERCOOKED_LEVELS_DIR at startup, so an image built around content that breaks a PLAN §8 rule exits 2 with the named-rule report on stderr instead of serving an empty lobby; with restart: unless-stopped that looks like a crash loop whose logs are the diagnosis. uv run undercooked-levels validate reproduces 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.