# Preview Deployments `src/preview/` — Docker-based preview deployments for reviewing changes in a running application. ## Overview When a phase enters `pending_review`, reviewers can spin up the app at a specific branch in local Docker containers, accessible through a single port via a Caddy reverse proxy. **Key design decision: No database table.** Docker IS the source of truth. Instead of persisting rows, we query Docker directly via compose project names, container labels, and `docker compose` CLI commands. ## Architecture ``` PreviewManager ├── ConfigReader (discover .cw-preview.yml / compose / Dockerfile) ├── ComposeGenerator (generate docker-compose.yml + Caddyfile) ├── DockerClient (thin wrapper around docker compose CLI) ├── HealthChecker (poll service healthcheck endpoints) └── PortAllocator (find next available port 9100-9200) ``` ### Lifecycle 1. **Start**: discover config → allocate port → generate compose + Caddyfile → `docker compose up --build -d` → health check → emit `preview:ready` 2. **Stop**: `docker compose down --volumes --remove-orphans` → clean up `.cw-previews//` → emit `preview:stopped` 3. **List**: `docker compose ls --filter name=cw-preview` → parse container labels → reconstruct status 4. **Shutdown**: `stopAll()` called on server shutdown to prevent orphaned containers ### Docker Labels All preview containers get `cw.*` labels for metadata retrieval: | Label | Purpose | |-------|---------| | `cw.preview` | `"true"` — marker for filtering | | `cw.initiative-id` | Initiative ID | | `cw.phase-id` | Phase ID (optional) | | `cw.project-id` | Project ID | | `cw.branch` | Branch name | | `cw.port` | Host port | | `cw.preview-id` | Nanoid for this deployment | ### Compose Project Naming Project names follow `cw-preview-` convention. This enables filtering via `docker compose ls --filter name=cw-preview`. ## Configuration Preview configuration is discovered from the project directory in this order: ### 1. `.cw-preview.yml` (explicit CW config) ```yaml version: 1 services: frontend: build: context: . dockerfile: packages/web/Dockerfile port: 3000 route: / healthcheck: path: / interval: 5s retries: 10 env: VITE_API_URL: /api backend: build: context: . dockerfile: packages/api/Dockerfile port: 8080 route: /api healthcheck: path: /health env: DATABASE_URL: postgres://db:5432/app db: image: postgres:16-alpine port: 5432 internal: true # not exposed through proxy env: POSTGRES_PASSWORD: preview ``` ### 2. `docker-compose.yml` / `compose.yml` (existing compose passthrough) If found, the existing compose file is wrapped with a Caddy sidecar. ### 3. `Dockerfile` (single-service fallback) If only a Dockerfile exists, creates a single `app` service building from `.` with port 3000. ## Reverse Proxy: Caddy Caddy runs as a container in the same Docker network. Only Caddy publishes a port to the host. Generated Caddyfile: ``` :80 { handle_path /api/* { reverse_proxy backend:8080 } handle { reverse_proxy frontend:3000 } } ``` ## Module Files | File | Purpose | |------|---------| | `types.ts` | PreviewConfig, PreviewStatus, labels, constants | | `config-reader.ts` | Discovery + YAML parsing | | `compose-generator.ts` | Docker Compose YAML + Caddyfile generation | | `docker-client.ts` | Docker CLI wrapper (execa) | | `health-checker.ts` | Service readiness polling | | `port-allocator.ts` | Port 9100-9200 allocation with bind test | | `manager.ts` | PreviewManager class (start/stop/list/status/stopAll) | | `index.ts` | Barrel exports | ## Events | Event | Payload | |-------|---------| | `preview:building` | `{previewId, initiativeId, branch, port}` | | `preview:ready` | `{previewId, initiativeId, branch, port, url}` | | `preview:stopped` | `{previewId, initiativeId}` | | `preview:failed` | `{previewId, initiativeId, error}` | ## tRPC Procedures | Procedure | Type | Input | |-----------|------|-------| | `startPreview` | mutation | `{initiativeId, phaseId?, projectId, branch}` | | `stopPreview` | mutation | `{previewId}` | | `listPreviews` | query | `{initiativeId?}` | | `getPreviewStatus` | query | `{previewId}` | ## CLI Commands ``` cw preview start --initiative --project --branch [--phase ] cw preview stop cw preview list [--initiative ] cw preview status ``` ## Frontend `PreviewPanel` component in the Review tab: - **No preview**: "Start Preview" button - **Building**: Spinner + "Building preview..." - **Running**: Green dot + `http://localhost:` link + Stop button - **Failed**: Error message + Retry button Polls `getPreviewStatus` with `refetchInterval: 3000` while active. ## Container Wiring - `PreviewManager` instantiated in `src/container.ts` with `(projectRepository, eventBus, workspaceRoot)` - Added to `Container` interface and `toContextDeps()` - `GracefulShutdown` calls `previewManager.stopAll()` during shutdown - `requirePreviewManager(ctx)` helper in `src/trpc/routers/_helpers.ts` ## Dependencies - `js-yaml` + `@types/js-yaml` — for parsing `.cw-preview.yml` - Docker must be installed and running on the host