---
phase: 5
slug: deploy
status: draft
nyquist_compliant: true
wave_0_complete: true
created: 2026-05-08
---

# Phase 5 — Validation Strategy

> Per-phase validation contract for feedback sampling during execution.

---

## Test Infrastructure

| Property | Value |
|----------|-------|
| **Framework** | vitest 1.x (Phase 4 inherited) + GitHub Actions composite (verify-phase-5.yml) + lint-deploy-stack.mjs drift-guard |
| **Config file** | `apps/server/vitest.config.ts`, `.github/workflows/verify-phase-5.yml`, `tools/scripts/lint-deploy-stack.mjs` |
| **Quick run command** | `pnpm --filter @rebno/server test:unit` |
| **Full suite command** | `pnpm verify:phase-5` (typecheck + unit + integ + trace:check + lint-deploy-stack + Dockerfile-build dry run) |
| **Estimated runtime** | ~90s local; ~5 min in CI (deploy-staging path); 30 min for nightly soak (separate workflow) |

---

## Sampling Rate

- **After every task commit:** Run `pnpm --filter @rebno/server test:unit` (or plan-specific quick command — see per-task map)
- **After every plan wave:** Run `pnpm verify:phase-5`
- **Before `/gsd-verify-work`:** Full suite must be green; first staging deploy must complete; RESTORE.md drill validated; Phase 4 carry-forward (kill -9, argon2 bench, multi-client smoke) closed and recorded in 05-HUMAN-UAT.md
- **Max feedback latency:** ~90s for unit; ~5 min for full CI; 30 min nightly soak runs out-of-band (failure → GH Issue, not blocking)

---

## Per-Task Verification Map

> Filled by gsd-planner during planning. Each plan's `<validation>` block populates a row here. Below are placeholder rows — planner replaces with concrete `{N}-{plan}-{task}` entries.

| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------|
| 5-01-01 | 01 | 1 | REQ-DEP-01 | — | Dockerfile builds with argon2 + better-sqlite3 native against Node 22 | int | `docker build --target runtime apps/server` | ❌ W0 | ⬜ pending |
| 5-02-01 | 02 | 1 | REQ-DEP-02 | — | fly.staging.toml + fly.prod.toml parse via `flyctl config validate` | unit | `flyctl config validate -c apps/server/fly.staging.toml && flyctl config validate -c apps/server/fly.prod.toml` | ❌ W0 | ⬜ pending |
| 5-03-01 | 03 | 1 | REQ-DEP-03 | — | litestream.yml replicates WAL to Tigris; pre-migrate snapshot succeeds | int | `pnpm test:litestream-restore` (Wave 0) | ❌ W0 | ⬜ pending |
| 5-04-01 | 04 | 1 | REQ-DEP-04 | — | trace:check is required PR check + verify-phase-N integration | unit | `pnpm trace:check:json` | ✅ | ⬜ pending |
| 5-05-01 | 05 | 2 | REQ-DEP-04 | — | deploy-staging.yml builds + pushes + deploys image-by-SHA | int | `act -W .github/workflows/deploy-staging.yml --dry-run` (or scripted assertion) | ❌ W0 | ⬜ pending |
| 5-06-01 | 06 | 2 | REQ-DEP-04 | — | deploy-prod.yml resolves staging SHA on tag push and re-deploys | int | dry-run flyctl + assertion harness | ❌ W0 | ⬜ pending |
| 5-07-01 | 07 | 1 | REQ-DEP-05 | — | /health reports {status, ws_ready, rooms_loaded}; Fly check passes | unit | `pnpm --filter @rebno/server test -- health.test.ts` | ❌ W0 (extends Phase 4) | ⬜ pending |
| 5-08-01 | 08 | 2 | REQ-DEP-06 | — | otel-init.ts boots SDK; pino redact preserved; OTLP exporter target correct; SDK init failure does NOT crash server | unit + int | `pnpm --filter @rebno/server test -- otel-init.test.ts` | ❌ W0 | ⬜ pending |
| 5-09-01 | 09 | 2 | REQ-DEP-06 | — | apps/obs deploys; ZO_S3_* + ZO_ROOT_USER_PASSWORD wired; OTLP-HTTP ingest accepts traces | int | `pnpm test:obs-ingest` (smoke against deployed staging) | ❌ W0 | ⬜ pending |
| 5-10-01 | 10 | 2 | (D-04) | T-DEP-04 | STAGING_INVITE_TOKEN middleware rejects WS handshakes lacking ?invite= when STAGING_MODE=1; no-op when unset | unit | `pnpm --filter @rebno/server test -- staging-invite.test.ts` | ❌ W0 | ⬜ pending |
| 5-11-01 | 11 | 3 | REQ-DEP-08 | — | 30-min 2-client soak: zero spurious WS disconnects, zero baseline RATE_LIMITED, p95 tick < 25ms | int | `pnpm soak:staging` (out-of-band, nightly + manual dispatch) | ❌ W0 | ⬜ pending (manual + nightly) |
| 5-12-01 | 12 | 1 | REQ-DEP-04 | — | run-migrations.ts seeds __drizzle_migrations idempotently; replaces bootstrapSchemaIfFresh | unit + int | `pnpm --filter @rebno/server test -- run-migrations.test.ts` | ❌ W0 | ⬜ pending |
| 5-13-01 | 13 | 3 | REQ-DEP-07 | — | RESTORE.md drill: cold restore on staging Fly machine → server up + state intact in <5 min; recorded in 05-HUMAN-UAT.md | manual | RESTORE.md §"Drill Procedure" steps | ❌ W0 (HUMAN-UAT) | ⬜ pending (manual) |
| 5-14-01 | 14 | 3 | REQ-DEP-04 | — | verify-phase-5.{mjs,test.mjs,yml} composite gate green; ADR 0005 + ADR 0006 created | unit + doc | `node scripts/verify-phase-5.mjs` | ❌ W0 | ⬜ pending |

*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*

> **Note:** Planner MUST replace placeholders with concrete task IDs and per-task commands. Wave 0 entries (❌ W0) drive Plan 00 / first-wave file creation.

---

## Wave 0 Requirements

- [ ] `apps/server/test/staging-invite.test.ts` — REQ middleware unit fixtures
- [ ] `apps/server/test/otel-init.test.ts` — OTel SDK boot fixture (mock OTLP exporter)
- [ ] `apps/server/test/run-migrations.test.ts` — drizzle-kit programmatic migrator + __drizzle_migrations seeding fixtures
- [ ] `apps/server/test/health.fly.test.ts` — extends Phase 4 health.test.ts with draining-state assertion
- [ ] `tools/scripts/lint-deploy-stack.mjs` — drift-guard for fly.{staging,prod}.toml + Dockerfile + litestream.yml + apps/obs/fly.toml + apps/obs/Dockerfile
- [ ] `tools/scripts/lint-deploy-stack.test.mjs` — unit fixtures for the linter (golden + drift cases)
- [ ] `scripts/soak-staging.mjs` — 30-min 2-client harness (extends apps/server/test/authority.integ.test.ts pattern)
- [ ] `scripts/verify-phase-5.mjs` — composite gate runner
- [ ] `scripts/test-litestream-restore.mjs` — Wave 0 fixture: simulated `/data` cold restore from Tigris (uses local minio container or Tigris staging credentials)
- [ ] `.github/workflows/verify-phase-5.yml` — composite CI gate
- [ ] `.github/workflows/trace-check.yml` — required PR check (DEP-04 hard-gate placement)
- [ ] `.github/workflows/soak-staging.yml` — nightly + workflow_dispatch 30-min soak

---

## Manual-Only Verifications

| Behavior | Requirement | Why Manual | Test Instructions |
|----------|-------------|------------|-------------------|
| RESTORE.md drill executed end-to-end on rebno-staging in <5 min | REQ-DEP-07 | Cold-restore drill requires destroying + restoring an actual Fly Volume + observing recovery time; not safe to automate in CI against shared staging | Follow `RESTORE.md` §"Drill Procedure"; record start/end timestamps in `05-HUMAN-UAT.md` Test 1 |
| Phase 4 SRV-08 carry-forward: kill -9 mid-tick on rebno-staging | (Phase 4 carry-forward) | Requires `fly ssh console` + manual SIGKILL during active tick; closes Phase 4 debt on first staging deploy | Verbatim 6-step procedure from `04-09-SUMMARY.md` §"Manual Verification (Phase 5 Debt)"; record in `05-HUMAN-UAT.md` Test 2 |
| Phase 4 argon2id prod-hardware bench on Fly shared-cpu-2x | (Phase 4 carry-forward) | Bench must run on actual prod hardware to measure real argon2 cost; if mean <200ms, bump memoryCost or timeCost | `fly ssh console -a rebno-staging -C 'cd /app && node argon2-bench.mjs'`; record numbers in `05-HUMAN-UAT.md` Test 3; if action required, file follow-up |
| Phase 4 multi-client move+chat smoke on deployed staging | (Phase 4 carry-forward) | First end-to-end real-client+real-server traffic test; doubles as DEP-08 idle-timeout sanity check before automated 30-min soak | Two browser tabs against `wss://rebno-staging.fly.dev/colyseus` with `?invite=<token>`; observe move + chat for 5 min; record in `05-HUMAN-UAT.md` Test 4 |
| Legacy localList.txt one-shot import via fly ssh sftp | (D-17) | Plaintext-credentials ingest must NEVER enter image registry; one-time per env via manual sftp-then-delete ritual | RESTORE.md §"Legacy Credentials Import" steps; record in `05-HUMAN-UAT.md` Test 5 |
| Ed25519 pubkey extraction from rebno-prod /data/keys/room_signing.pub for Phase 6 client builds | (D-19) | One-shot manual op per env; pubkey then baked into Phase 6 Vite env | `fly ssh console -a rebno-prod -C 'cat /data/keys/room_signing.pub'`; record value in `05-HUMAN-UAT.md` Test 6 |
| Fly proxy IP allowlist for staging + obs UI | (D-04, D-15) | Fly `fly ips allocate --network` is operator-side config; not in repo | Allocate operator IPs per RESTORE.md §"Access Hardening"; record in `05-HUMAN-UAT.md` Test 7 |

---

## Validation Sign-Off

- [ ] All tasks have `<automated>` verify or Wave 0 dependencies
- [ ] Sampling continuity: no 3 consecutive tasks without automated verify
- [ ] Wave 0 covers all MISSING references
- [ ] No watch-mode flags
- [ ] Feedback latency <90s local, <5 min CI deploy path, 30 min nightly soak (out-of-band)
- [ ] `nyquist_compliant: true` set in frontmatter once planner has populated per-task rows with concrete commands

**Approval:** approved (Plan 14)
**Approved on:** 2026-05-08
**Approved by:** verify-phase-5.mjs (composite gate green)
