Files
undercooked/docs/DEPLOYMENT.md
T
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

54 lines
2.9 KiB
Markdown

# 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`.
```bash
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.