- Add agentId label to preview containers (cw.agent-id) for tracking - Add startForAgent/stopByAgentId methods to PreviewManager - Auto-teardown: previews torn down on agent:stopped event - Conditional preview prompt injection for execute/refine/discuss agents - Agent-simplified CLI: cw preview start/stop --agent <id> - cw preview setup command with --auto mode for guided config generation - hasPreviewConfig hint on cw project register output - New tRPC procedures: startPreviewForAgent, stopPreviewByAgent
446 lines
17 KiB
Markdown
446 lines
17 KiB
Markdown
# Preview Deployments
|
|
|
|
`apps/server/preview/` — Docker-based preview deployments for reviewing changes in a running application.
|
|
|
|
## Overview
|
|
|
|
Preview deployments let reviewers spin up a branch in local Docker containers. A single shared **Caddy gateway** handles subdomain routing for all active previews, accessed at `http://<previewId>.localhost:<gatewayPort>`.
|
|
|
|
Two modes:
|
|
- **Preview mode**: Checks out target branch into a git worktree, builds Docker images, serves production-like output.
|
|
- **Dev mode**: Mounts the agent's worktree into a container with a dev server image (e.g. `node:20-alpine`), enabling hot reload.
|
|
|
|
**Auto-start**: When a phase enters `pending_review`, a preview is automatically started for the phase's branch (if the initiative has exactly one project).
|
|
|
|
**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.
|
|
|
|
## Setting Up Preview Deployments for a Project
|
|
|
|
### Prerequisites
|
|
|
|
- **Docker** installed and running (`docker --version` / `docker compose version`)
|
|
- **Project registered** in Codewalkers (`cw project add` or via the UI)
|
|
- **Browser**: Chrome or Firefox recommended. Safari requires manual `/etc/hosts` entries for `*.localhost` subdomains.
|
|
|
|
### Quick Start
|
|
|
|
1. **Add a `.cw-preview.yml`** to your project root. This tells Codewalkers how to build and run your app:
|
|
|
|
```yaml
|
|
version: 1
|
|
services:
|
|
app:
|
|
build: .
|
|
port: 3000
|
|
```
|
|
|
|
That's it for a single-service app with a Dockerfile. Run:
|
|
|
|
```sh
|
|
cw preview start --initiative <id> --project <id> --branch <branch>
|
|
```
|
|
|
|
Open the URL printed in the output (e.g. `http://abc123.localhost:9100`).
|
|
|
|
### Don't Have a `.cw-preview.yml`?
|
|
|
|
The config reader auto-discovers in this order:
|
|
|
|
1. **`.cw-preview.yml`** — full control (recommended)
|
|
2. **`docker-compose.yml` / `compose.yml`** — uses your existing compose file
|
|
3. **`Dockerfile`** — builds a single `app` service on port 3000
|
|
|
|
If your project already has a Dockerfile or compose file, previews work out of the box with zero config.
|
|
|
|
### Multi-Service Example
|
|
|
|
A typical full-stack app with frontend, backend, and database:
|
|
|
|
```yaml
|
|
version: 1
|
|
services:
|
|
frontend:
|
|
build:
|
|
context: .
|
|
dockerfile: apps/web/Dockerfile
|
|
port: 3000
|
|
route: / # serves the root path
|
|
healthcheck:
|
|
path: /
|
|
interval: 5s
|
|
retries: 10
|
|
env:
|
|
VITE_API_URL: /api
|
|
dev: # optional: used in dev mode
|
|
image: node:20-alpine
|
|
command: npm run dev -- --host 0.0.0.0
|
|
workdir: /app
|
|
|
|
backend:
|
|
build:
|
|
context: .
|
|
dockerfile: packages/api/Dockerfile
|
|
port: 8080
|
|
route: /api # proxied under /api/*
|
|
healthcheck:
|
|
path: /health
|
|
env:
|
|
DATABASE_URL: postgres://db:5432/app
|
|
|
|
db:
|
|
image: postgres:16-alpine
|
|
port: 5432
|
|
internal: true # not exposed through the proxy
|
|
env:
|
|
POSTGRES_PASSWORD: preview
|
|
```
|
|
|
|
Requests to `http://<id>.localhost:9100/` hit `frontend:3000`, requests to `/api/*` hit `backend:8080`, and `db` is only reachable by other services.
|
|
|
|
### Config Reference
|
|
|
|
Each service in `.cw-preview.yml` supports:
|
|
|
|
| Field | Required | Description |
|
|
|-------|----------|-------------|
|
|
| `build` | yes* | Build context — string (`"."`) or object (`{context, dockerfile}`) |
|
|
| `image` | yes* | Docker image to pull (e.g. `postgres:16-alpine`) |
|
|
| `port` | yes** | Container port the service listens on |
|
|
| `route` | no | URL path prefix for gateway routing (default: `/`) |
|
|
| `internal` | no | If `true`, not exposed through the proxy (e.g. databases) |
|
|
| `healthcheck` | no | `{path, interval?, retries?}` — polled before marking ready |
|
|
| `env` | no | Environment variables passed to the container |
|
|
| `volumes` | no | Additional volume mounts |
|
|
| `seed` | no | Array of shell commands to run inside the container after health checks pass |
|
|
| `dev` | no | Dev mode overrides: `{image, command?, workdir?}` |
|
|
|
|
\* Provide either `build` or `image`, not both.
|
|
\** Required unless `internal: true`.
|
|
|
|
### Seeding
|
|
|
|
If a service needs initialization (database migrations, fixture loading, etc.), add a `seed` array. Commands run inside the container via `docker compose exec` after all health checks pass, before the preview is marked ready.
|
|
|
|
```yaml
|
|
services:
|
|
app:
|
|
build: .
|
|
port: 3000
|
|
seed:
|
|
- npm run db:migrate
|
|
- npm run db:seed
|
|
```
|
|
|
|
Seeds execute in service definition order. Each command has a 5-minute timeout. If any seed command fails (non-zero exit), the preview fails and all containers are cleaned up.
|
|
|
|
### Dev Mode
|
|
|
|
Dev mode skips the Docker build and instead mounts your source code into a container running a dev server. Useful for hot reload during active development.
|
|
|
|
To use dev mode, add a `dev` section to the service:
|
|
|
|
```yaml
|
|
services:
|
|
app:
|
|
build: .
|
|
port: 3000
|
|
dev:
|
|
image: node:20-alpine # base image with your runtime
|
|
command: npm run dev -- --host 0.0.0.0 # dev server command
|
|
workdir: /app # where source is mounted
|
|
```
|
|
|
|
Start with:
|
|
|
|
```sh
|
|
cw preview start --initiative <id> --project <id> --branch <branch> --mode dev
|
|
```
|
|
|
|
The project directory is mounted at `workdir` (default `/app`). An anonymous volume is created for `node_modules` to prevent host/container conflicts.
|
|
|
|
### Healthchecks
|
|
|
|
If a service has a `healthcheck`, the preview waits for it to respond with HTTP 200 before reporting ready. Without a healthcheck, the service is considered ready as soon as the container starts.
|
|
|
|
```yaml
|
|
healthcheck:
|
|
path: /health # required: endpoint to poll
|
|
interval: 5s # optional: time between checks (default: 5s)
|
|
retries: 10 # optional: max attempts before failing (default: 10)
|
|
```
|
|
|
|
### Auto-Start on Review
|
|
|
|
When a phase transitions to `pending_review`, a preview is automatically started if:
|
|
- The initiative has exactly one registered project
|
|
- The project has a discoverable config (`.cw-preview.yml`, compose file, or Dockerfile)
|
|
|
|
No manual `cw preview start` needed — just push your branch and move the phase to review.
|
|
|
|
## Architecture
|
|
|
|
```
|
|
.cw-previews/
|
|
gateway/
|
|
docker-compose.yml ← single Caddy container, one port
|
|
Caddyfile ← regenerated on each preview add/remove
|
|
<previewId>/
|
|
docker-compose.yml ← per-preview stack (no published ports)
|
|
source/ ← git worktree (preview mode only)
|
|
|
|
Docker:
|
|
network: cw-preview-net ← external, shared by gateway + all previews
|
|
cw-preview-gateway ← Caddy on one port, subdomain routing
|
|
cw-preview-<id> ← per-preview compose project (services only)
|
|
|
|
Routing:
|
|
<previewId>.localhost:<gatewayPort> → cw-preview-<id>-<service>:<port>
|
|
```
|
|
|
|
```
|
|
PreviewManager
|
|
├── GatewayManager (shared Caddy gateway lifecycle + Caddyfile generation)
|
|
├── ConfigReader (discover .cw-preview.yml / compose / Dockerfile)
|
|
├── ComposeGenerator (generate per-preview docker-compose.yml)
|
|
├── DockerClient (thin wrapper around docker compose CLI + network ops)
|
|
├── HealthChecker (poll service healthcheck endpoints via subdomain URL)
|
|
├── PortAllocator (find next available port 9100-9200 for gateway)
|
|
└── Worktree helper (git worktree add/remove for preview mode)
|
|
```
|
|
|
|
### Lifecycle
|
|
|
|
1. **Start**: ensure gateway → discover config → create worktree (preview) or use provided path (dev) → generate compose → `docker compose up --build -d` → update gateway routes → health check → run seed commands → emit `preview:ready`
|
|
2. **Stop**: `docker compose down --volumes --remove-orphans` → remove worktree → clean up `.cw-previews/<id>/` → update gateway routes → stop gateway if no more previews → emit `preview:stopped`
|
|
3. **List**: `docker compose ls --filter name=cw-preview` → skip gateway project → parse container labels → reconstruct status
|
|
4. **Shutdown**: `stopAll()` called on server shutdown — stops all previews, then stops gateway
|
|
|
|
### Gateway
|
|
|
|
The `GatewayManager` class manages a single shared Caddy container:
|
|
|
|
- **`ensureGateway()`** — idempotent. Creates the `cw-preview-net` Docker network, checks if gateway is already running, allocates a port (9100-9200) if needed, writes compose + Caddyfile, starts Caddy with `--watch` flag.
|
|
- **`updateRoutes()`** — regenerates the full Caddyfile from all active previews. Caddy's `--watch` flag auto-reloads on file change (no `docker exec` needed).
|
|
- **`stopGateway()`** — composes down the gateway, removes the Docker network, cleans up the gateway directory.
|
|
|
|
Gateway Caddyfile format:
|
|
```
|
|
{
|
|
auto_https off
|
|
}
|
|
|
|
abc123.localhost:9100 {
|
|
handle_path /api/* {
|
|
reverse_proxy cw-preview-abc123-backend:8080
|
|
}
|
|
handle {
|
|
reverse_proxy cw-preview-abc123-frontend:3000
|
|
}
|
|
}
|
|
|
|
xyz789.localhost:9100 {
|
|
handle {
|
|
reverse_proxy cw-preview-xyz789-app:3000
|
|
}
|
|
}
|
|
```
|
|
|
|
Routes are sorted by specificity (longer paths first) to ensure correct matching.
|
|
|
|
### Subdomain Routing
|
|
|
|
Previews are accessed at `http://<previewId>.localhost:<gatewayPort>`.
|
|
|
|
- **Chrome / Firefox**: Resolve `*.localhost` to `127.0.0.1` natively. No DNS config needed.
|
|
- **Safari**: Requires a `/etc/hosts` entry: `127.0.0.1 <previewId>.localhost` for each preview.
|
|
|
|
### 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` | Gateway port |
|
|
| `cw.preview-id` | Nanoid for this deployment |
|
|
| `cw.mode` | `"preview"` or `"dev"` |
|
|
| `cw.agent-id` | Agent ID (optional, set when started via `--agent`) |
|
|
|
|
### Compose Project Naming
|
|
|
|
- **Gateway**: `cw-preview-gateway` (single instance)
|
|
- **Previews**: `cw-preview-<nanoid>` — filtered via `docker compose ls --filter name=cw-preview`, gateway excluded from listings
|
|
- **Container names**: `cw-preview-<id>-<service>` — unique DNS names on the shared network
|
|
|
|
### Networking
|
|
|
|
- **`cw-preview-net`** — external Docker bridge network shared by gateway + all preview stacks
|
|
- **`internal`** — per-preview bridge network for inter-service communication
|
|
- Public services join both networks; internal services (e.g. databases) only join `internal`
|
|
|
|
## Config Discovery
|
|
|
|
See [Setting Up Preview Deployments](#setting-up-preview-deployments-for-a-project) above for the full config reference. Discovery order:
|
|
|
|
1. **`.cw-preview.yml`** — explicit CW preview config (recommended)
|
|
2. **`docker-compose.yml` / `compose.yml`** — existing compose file with gateway network injection
|
|
3. **`Dockerfile`** — single-service fallback (builds from `.`, assumes port 3000)
|
|
|
|
## Module Files
|
|
|
|
| File | Purpose |
|
|
|------|---------|
|
|
| `types.ts` | PreviewConfig, PreviewStatus, labels, constants, dev config types |
|
|
| `config-reader.ts` | Discovery + YAML parsing (including `dev` section) |
|
|
| `compose-generator.ts` | Per-preview Docker Compose YAML + label generation |
|
|
| `gateway.ts` | GatewayManager class + Caddyfile generation |
|
|
| `worktree.ts` | Git worktree create/remove helpers |
|
|
| `docker-client.ts` | Docker CLI wrapper (execa) + network operations |
|
|
| `health-checker.ts` | Service readiness polling via subdomain URL |
|
|
| `port-allocator.ts` | Port 9100-9200 allocation with TCP bind test |
|
|
| `manager.ts` | PreviewManager class (start/stop/list/status/stopAll + auto-start) |
|
|
| `index.ts` | Barrel exports |
|
|
|
|
## Events
|
|
|
|
| Event | Payload |
|
|
|-------|---------|
|
|
| `preview:building` | `{previewId, initiativeId, branch, gatewayPort, mode, phaseId?}` |
|
|
| `preview:ready` | `{previewId, initiativeId, branch, gatewayPort, url, mode, phaseId?}` |
|
|
| `preview:stopped` | `{previewId, initiativeId}` |
|
|
| `preview:failed` | `{previewId, initiativeId, error}` |
|
|
|
|
## Auto-Start
|
|
|
|
`PreviewManager.setupEventListeners()` listens for `phase:pending_review` events:
|
|
1. Loads the initiative and its projects
|
|
2. If exactly one project: auto-starts a preview in `preview` mode
|
|
3. Branch is derived from `phaseBranchName(initiative.branch, phase.name)`
|
|
4. Errors are caught and logged (best-effort, never blocks the phase transition)
|
|
|
|
## Agent Integration
|
|
|
|
Agents can spin up and tear down preview deployments using simplified commands that only require their agent ID — the server resolves initiative, project, branch, and mode automatically.
|
|
|
|
### Agent-Simplified Commands
|
|
|
|
When an agent has a `<preview_deployments>` section in its prompt (injected automatically if the initiative's project has `.cw-preview.yml`), it can use:
|
|
|
|
```
|
|
cw preview start --agent <agentId> # Server resolves everything, starts dev mode
|
|
cw preview stop --agent <agentId> # Stops all previews for this agent
|
|
```
|
|
|
|
### Prompt Injection
|
|
|
|
Preview instructions are automatically appended to agent prompts when all conditions are met:
|
|
1. Agent mode is `execute`, `refine`, or `discuss`
|
|
2. Agent has an `initiativeId`
|
|
3. Initiative has exactly one linked project
|
|
4. Project clone directory contains `.cw-preview.yml`
|
|
|
|
The injected `<preview_deployments>` block includes prefilled `cw preview start/stop --agent <agentId>` commands.
|
|
|
|
### Agent ID Label
|
|
|
|
Previews started with `--agent` receive a `cw.agent-id` Docker container label. This enables:
|
|
- **Auto-teardown**: When an agent stops (`agent:stopped` event), all previews labeled with its ID are automatically torn down (best-effort).
|
|
- **Agent-scoped stop**: `cw preview stop --agent <id>` finds and stops previews by label.
|
|
|
|
### Setup Command
|
|
|
|
`cw preview setup` prints inline setup instructions for `.cw-preview.yml`. With `--auto --project <id>`, it creates an initiative and spawns a refine agent to analyze the project and generate the config file.
|
|
|
|
## tRPC Procedures
|
|
|
|
| Procedure | Type | Input |
|
|
|-----------|------|-------|
|
|
| `startPreview` | mutation | `{initiativeId, phaseId?, projectId, branch, mode?, worktreePath?, agentId?}` |
|
|
| `startPreviewForAgent` | mutation | `{agentId}` |
|
|
| `stopPreview` | mutation | `{previewId}` |
|
|
| `stopPreviewByAgent` | mutation | `{agentId}` |
|
|
| `listPreviews` | query | `{initiativeId?}` |
|
|
| `getPreviewStatus` | query | `{previewId}` |
|
|
|
|
`mode` defaults to `'preview'`. Set to `'dev'` with a `worktreePath` for dev mode. `startPreviewForAgent` always uses dev mode.
|
|
|
|
## CLI Commands
|
|
|
|
```
|
|
cw preview start --initiative <id> --project <id> --branch <branch> [--phase <id>]
|
|
cw preview start --agent <id>
|
|
cw preview stop <previewId>
|
|
cw preview stop --agent <id>
|
|
cw preview list [--initiative <id>]
|
|
cw preview status <previewId>
|
|
cw preview setup [--auto --project <id> [--provider <name>]]
|
|
```
|
|
|
|
## Frontend
|
|
|
|
The Review tab shows preview status inline:
|
|
- **No preview**: "Start Preview" button
|
|
- **Building**: Spinner + "Building preview..."
|
|
- **Running**: Green dot + `http://<id>.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, phaseRepository, initiativeRepository)`
|
|
- Added to `Container` interface and `toContextDeps()`
|
|
- `GracefulShutdown` calls `previewManager.stopAll()` during shutdown
|
|
- `requirePreviewManager(ctx)` helper in `apps/server/trpc/routers/_helpers.ts`
|
|
|
|
## Codewalkers Self-Preview
|
|
|
|
Codewalkers itself has a Dockerfile, `.cw-preview.yml`, and seed script for preview deployments. This lets you demo the full app — initiatives, phases, tasks, agents with output, pages, and review tab with real git diffs.
|
|
|
|
### Files
|
|
|
|
| File | Purpose |
|
|
|------|---------|
|
|
| `Dockerfile` | Multi-stage: deps → build (server+web) → production (Node + Caddy) |
|
|
| `.cw-preview.yml` | Preview config: build, healthcheck, seed |
|
|
| `scripts/Caddyfile` | Caddy config: SPA file server + tRPC reverse proxy |
|
|
| `scripts/entrypoint.sh` | Init workspace, start backend, run Caddy |
|
|
| `scripts/seed-preview.sh` | Create demo git repo + run Node.js seed |
|
|
| `apps/server/preview-seed.ts` | Populate DB with demo initiative, phases, tasks, agents, log chunks, pages, review comments |
|
|
|
|
### Container Architecture
|
|
|
|
Single container running two processes:
|
|
- **Node.js backend** on port 3847 (tRPC API + health endpoint)
|
|
- **Caddy** on port 3000 (SPA file server, reverse proxy `/trpc` and `/health` to backend)
|
|
|
|
### Seed Data
|
|
|
|
The seed creates a "Task Manager Redesign" initiative with:
|
|
- 3 phases (completed, pending_review, in_progress)
|
|
- 9 tasks across phases
|
|
- 3 agents with JSONL log output
|
|
- Root page with Tiptap content
|
|
- 3 review comments on the pending_review phase
|
|
- Git repo with 3 branches and real commit diffs for the review tab
|
|
|
|
### Manual Testing
|
|
|
|
```sh
|
|
docker build -t cw-preview .
|
|
docker run -p 3000:3000 cw-preview
|
|
# Wait for startup, then seed:
|
|
docker exec <container> sh /app/scripts/seed-preview.sh
|
|
# Browse http://localhost:3000
|
|
```
|
|
|
|
## Dependencies
|
|
|
|
- `js-yaml` + `@types/js-yaml` — for parsing `.cw-preview.yml`
|
|
- `simple-git` — for git worktree operations
|
|
- Docker must be installed and running on the host
|