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.
54 lines
2.9 KiB
Markdown
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.
|