docs(slice-4): BOOTSTRAP.md PPE runbook, version 0.4.0, README status
ci / check (pull_request) Has been cancelled
ci / check (push) Has been cancelled

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 23:36:24 -07:00
parent 22d738fc74
commit acff7ac588
4 changed files with 127 additions and 6 deletions
+4 -2
View File
@@ -27,5 +27,7 @@ complete entry-routing rule (landing / create-storefront / admin), and the INV-1
whole-flow bootstrap test — all skinned to the Claude Design export
(`wiggleverse-ecomm-content/ui/designs/ecomm-login-and-create-storefront-designs/`),
which also re-skinned the SLICE-2 Landing + Sign-in screens. See
[`docs/BOOTSTRAP.md`](./docs/BOOTSTRAP.md) to run it locally. Deployed environments
(SLICE-4) are next.
[`docs/BOOTSTRAP.md`](./docs/BOOTSTRAP.md) to run it locally. SLICE-4's deploy
contract is in place (versioned `/healthz`, backend-served SPA, `SmtpMailer` with
honest 502 on delivery failure); PPE provisioning + the bootstrap rehearsal are the
remaining SLICE-4 steps (BOOTSTRAP.md's PPE section is the runbook).
+67 -3
View File
@@ -4,8 +4,9 @@ Bringing an environment from **empty persistence** to "first merchant, first
storefront" through the product flows alone (SD-0001 BUC-5). Empty is a working state
(INV-1): the app applies its own schema migrations at startup; there is no seed step.
This document grows per environment. SLICE-1 ships the **localhost** section; the
pre-production and production sections land with SLICE-4.
This document grows per environment. SLICE-1 shipped the **localhost** section;
SLICE-4 adds **pre-production (PPE)**. The production section lands with the prod
stand-up.
## Localhost
@@ -61,4 +62,67 @@ environment starts from (BUC-5a).
`scripts/dev.sh` sets `ECOMM_DATABASE_URL` to the local compose DSN
(`postgresql://ecomm:ecomm@localhost:5432/ecomm`). No deployment shape is baked into
the app (INV-8); deployed environments supply this and other config from Secret
Manager (SLICE-4).
Manager (see PPE below).
## Pre-production (PPE)
PPE is stood up and deployed through the **launch-app / flotilla-core** suite only
(handbook §8.5 — provisioning is an operator-run gesture). The app's deployment
record is `deployment.toml` at this repo's root; flotilla-core consumes it.
### One-time provisioning (operator-run, in order)
1. **Cloud foundation**`scaffold-gcp-project`: the GCP project, its dedicated
`--no-activate` gcloud config, billing, core APIs, ADC quota pin.
2. **Managed database**`provision-datastore` (launch-app §6.6a, built for ecomm's
D-7/D-8): Cloud SQL for PostgreSQL 16, private-IP-only, automated backups +
point-in-time recovery; writes the full DSN to Secret Manager as
`<project>/ecomm-ppe-database-url`. Bound on the deployment record as
`ECOMM_DATABASE_URL`.
3. **Secrets** (references only — bytes go in via stdin, never through a session):
`ECOMM_SESSION_SECRET` (fresh random), `ECOMM_SMTP_PASSWORD` (the shared
Wiggleverse relay credential), and flotilla's own Gitea read token for this
private repo.
4. **VM + edge**`provision-vm`: the e2-micro, IAP-only SSH, nginx + Cloudflare
origin TLS, the systemd unit, DNS for `ecomm-ppe.wiggleverse.org`.
5. **Deployment record** — `flotilla-core deployment scaffold ecomm … -o
deployment.toml`, validate, commit it here, `deployment import --reconcile`.
### Deploy
Each release is a git tag `v<VERSION>` matching the repo-root `VERSION` file (the
pin). The one gesture, from the flotilla-core repo's venv:
```bash
CLOUDSDK_ACTIVE_CONFIG_NAME=ecomm .venv/bin/flotilla-core deploy ecomm
```
Nine phases, fail-stop; it ends by polling `https://ecomm-ppe.wiggleverse.org/healthz`
and requiring `status == "ok"` and `version == <target>`. Watch it yourself the same
way:
```bash
curl https://ecomm-ppe.wiggleverse.org/healthz
```
### Configuration surface (PPE values)
| Env var | Kind | Value |
| --- | --- | --- |
| `ECOMM_DATABASE_URL` | secret ref | `<project>/ecomm-ppe-database-url` (from provision-datastore) |
| `ECOMM_SESSION_SECRET` | secret ref | `<project>/ecomm-ppe-session-secret` |
| `ECOMM_SMTP_PASSWORD` | secret ref | the shared Wiggleverse relay credential |
| `ECOMM_MAILER` | overlay | `smtp` |
| `ECOMM_COOKIE_SECURE` | overlay | `1` |
| `ECOMM_SMTP_HOST` / `_PORT` / `_USER` / `_FROM` | overlay | the relay coordinates (non-secret) |
### The rehearsal (PUC-11)
From empty persistence: the deploy migrates the schema at startup (INV-7); then a
real sign-up → one-time code arriving by **real email** → create storefront → admin,
through the public flows alone. Record each rehearsal here when it happens.
## Production
Lands with the prod stand-up — the **identical gesture** on a prod deployment record
(BUC-5a): same provisioning skills, same deploy, same rehearsal.
@@ -0,0 +1,55 @@
# SLICE-4 (part 1) — Deploy-Contract Code Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Make ecomm deployable by flotilla-core's 9-phase gesture and rehearsable on PPE: versioned health, SPA served by the backend, real `SmtpMailer` with honest delivery failure (closes ecomm#7), per SD-0001 §7.2 (SLICE-4) — the code half. Provisioning (Cloud SQL via the new launch-app `provision-datastore`, VM, deployment.toml) and the PPE rehearsal follow as operator gestures in the same session.
**Anchor:** SD-0001 §7.2 SLICE-4 (R2a, checked this session). flotilla contract: flotilla-core SPEC §8.1 (checkout `v<VERSION>` tag → pip → `npm ci && npm run build` → write `backend/.env` → restart → verify `body.version == target && body.status == "ok"`); provision-vm: nginx proxies ALL routes to uvicorn `app.main:app` (WorkingDirectory `backend/`), so the backend must serve `frontend/dist`.
**Architecture:** `VERSION` at repo root is the single version source (healthz body + FastAPI version + the deploy pin). `create_app()` mounts `frontend/dist` (when present) after all API routes — SPA fallback via `StaticFiles(html=True)`. `SmtpMailer` is the second adapter of the existing mailer port (STARTTLS smtplib, config from `ECOMM_SMTP_*`, INV-8); delivery failure raises `MailerError``accounts.request_code` sends **before** commit (rollback on failure — no orphan code, no tripped cooldown; ecomm#7) → BFF surfaces `502 delivery_failed` (INV-9).
**Tech Stack:** stdlib `smtplib`/`email.message`; FastAPI `StaticFiles`; no new dependencies.
---
### Task 1: VERSION + versioned /healthz
**Files:** Create `VERSION` (root). Modify `backend/app/main.py`, `backend/tests/test_healthz.py`.
- [ ] Write `VERSION` containing `0.4.0`.
- [ ] Test first: `test_healthz_ok_on_migrated_empty_db` asserts `{"status": "ok", "version": "0.4.0"}` read from the VERSION file (compare against `(repo_root/"VERSION").read_text().strip()`, not a literal). Run → FAIL.
- [ ] `main.py`: add `_APP_VERSION = (Path(__file__).resolve().parents[2] / "VERSION").read_text().strip()` (fallback `"0.0.0"` when missing); healthz returns `{"status": "ok", "version": _APP_VERSION}`; `FastAPI(version=_APP_VERSION)`. Run → PASS. Commit.
### Task 2: backend serves the SPA (deploy phase-8 contract)
**Files:** Modify `backend/app/main.py`. Test `backend/tests/test_static_spa.py`.
- [ ] Test first: `create_app(database_url=..., static_dir=tmp_path)` with a `tmp_path/index.html`; GET `/` → 200 + the html; GET `/healthz` and `/api/auth/me` still answer JSON (API wins over the mount). Default `static_dir=None` → resolves `repo_root/frontend/dist`, skipped silently when absent (dev: Vite serves). Run → FAIL.
- [ ] `create_app(database_url=None, static_dir: str | Path | None = None)`; after the last route: resolve dir, `if dir/index.html exists: app.mount("/", StaticFiles(directory=dir, html=True), name="spa")`. Run → PASS (whole suite). Commit.
### Task 3: SmtpMailer + config surface (INV-8)
**Files:** Modify `backend/app/platform/{mailer,config}.py`. Test `backend/tests/test_mailer.py` (extend).
- [ ] Config additions: `smtp_host()` (`ECOMM_SMTP_HOST`), `smtp_port()` (`ECOMM_SMTP_PORT`, 587), `smtp_user()`, `smtp_password()`, `smtp_from()` (default = user), `smtp_starttls()` (default on).
- [ ] Tests first: `MailerError` exists; `build_mailer("smtp")` returns `SmtpMailer` wired from env (monkeypatched); `SmtpMailer.send` drives a monkeypatched `smtplib.SMTP` (starttls → login → send_message with To/Subject/From + body) and never logs the body; SMTP exception → `MailerError`. Run → FAIL.
- [ ] Implement: `MailerError(Exception)`; `SmtpMailer` (EmailMessage; `smtplib.SMTP(host, port, timeout=10)`, STARTTLS per config, login when user set, `send_message`; `except Exception → raise MailerError`; logs only `smtp sent to=<sha256[:8] of recipient>` — §6.6 log hygiene); `build_mailer("smtp")` builds it from config. Run → PASS. Commit.
### Task 4: honest delivery failure — send-before-commit + 502 (closes ecomm#7)
**Files:** Modify `backend/app/domains/accounts/{errors,service,__init__}.py`, `backend/app/main.py`. Test `backend/tests/test_accounts_request_code.py` + `test_auth_endpoints.py` (extend).
- [ ] Tests first: a mailer whose `send` raises `MailerError` → service raises `accounts.DeliveryFailed`, **no `auth_code` row remains**, and an immediate retry is **not** cooldown-blocked; endpoint test: 502 `{"error": {"code": "delivery_failed"}}`. Run → FAIL.
- [ ] Implement: `DeliveryFailed(AccountsError)`; `request_code` moves `conn.commit()` **after** `mailer.send(...)`, wrapping send in `try/except MailerError → conn.rollback(); raise DeliveryFailed`; BFF maps it to `_error(502, "delivery_failed", "We couldn't send the code — try again.")`. Run → PASS (whole backend suite). Commit.
### Task 5: BOOTSTRAP.md PPE section + housekeeping + gate
**Files:** Modify `docs/BOOTSTRAP.md`, `README.md`, `frontend/package.json` (0.4.0).
- [ ] BOOTSTRAP.md: replace the "land with SLICE-4" sentence; add **Pre-production (PPE)** section — prerequisites (suite-run provisioning: scaffold-gcp-project → provision-datastore (Cloud SQL, engineering#46) → provision-vm → define-deployment/import; secrets as references), the one deploy gesture (`CLOUDSDK_ACTIVE_CONFIG_NAME=<config> flotilla-core deploy <name>`), how to watch `/healthz`, the rehearsal walk (PUC-11), reset-to-empty note; Prod section: placeholder "lands with the prod stand-up" honestly.
- [ ] README status: deploy contract in place; frontend package 0.4.0.
- [ ] `./scripts/check.sh` → all green. Commit. Push, PR citing SD-0001 §7.2 SLICE-4 + ecomm#7, merge, **tag `v0.4.0` on the merge commit and push the tag** (the deploy pin).
## Self-review
Spec coverage: SmtpMailer + config wiring INV-8 ✓ (T3), §6.6 hardening — Secure cookies already config-driven, log hygiene ✓ (T3 no-body logging; LogMailer is dev-only by config), `deployment.toml` + provisioning deliberately deferred to the Phase-C suite gestures (needs the GCP project id that scaffold-gcp-project mints), BOOTSTRAP.md PPE ✓ (T5), versioned health for the §8.1 verify ✓ (T1), SPA serving for the §2 topology ✓ (T2), ecomm#7 ✓ (T4). E2E browser tests still deferred per §6.8. Type consistency: `MailerError` lives in platform/mailer; `DeliveryFailed` in accounts errors; both exported via package surfaces.
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "wiggleverse-ecomm-frontend",
"private": true,
"version": "0.3.0",
"version": "0.4.0",
"type": "module",
"scripts": {
"dev": "vite",