Files
Codewalkers/docs/preview.md
Lukas May 34578d39c6 refactor: Restructure monorepo to apps/server/ and apps/web/ layout
Move src/ → apps/server/ and packages/web/ → apps/web/ to adopt
standard monorepo conventions (apps/ for runnable apps, packages/
for reusable libraries). Update all config files, shared package
imports, test fixtures, and documentation to reflect new paths.

Key fixes:
- Update workspace config to ["apps/*", "packages/*"]
- Update tsconfig.json rootDir/include for apps/server/
- Add apps/web/** to vitest exclude list
- Update drizzle.config.ts schema path
- Fix ensure-schema.ts migration path detection (3 levels up in dev,
  2 levels up in dist)
- Fix tests/integration/cli-server.test.ts import paths
- Update packages/shared imports to apps/server/ paths
- Update all docs/ files with new paths
2026-03-03 11:22:53 +01:00

172 lines
5.2 KiB
Markdown

# Preview Deployments
`apps/server/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/<id>/` → 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-<nanoid>` 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: apps/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 <id> --project <id> --branch <branch> [--phase <id>]
cw preview stop <previewId>
cw preview list [--initiative <id>]
cw preview status <previewId>
```
## Frontend
`PreviewPanel` component in the Review tab:
- **No preview**: "Start Preview" button
- **Building**: Spinner + "Building preview..."
- **Running**: Green dot + `http://localhost:<port>` link + Stop button
- **Failed**: Error message + Retry button
Polls `getPreviewStatus` with `refetchInterval: 3000` while active.
## Container Wiring
- `PreviewManager` instantiated in `apps/server/container.ts` with `(projectRepository, eventBus, workspaceRoot)`
- Added to `Container` interface and `toContextDeps()`
- `GracefulShutdown` calls `previewManager.stopAll()` during shutdown
- `requirePreviewManager(ctx)` helper in `apps/server/trpc/routers/_helpers.ts`
## Dependencies
- `js-yaml` + `@types/js-yaml` — for parsing `.cw-preview.yml`
- Docker must be installed and running on the host