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
172 lines
5.2 KiB
Markdown
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
|