Job Definitions
Complete reference for every CI job -- steps, environment variables, permissions, and conditional execution.
Complete reference for all five jobs in .github/workflows/ci.yml. Each section lists the job's steps, environment variables, required permissions, and conditional logic.
Global Configuration
These settings apply to the entire workflow:
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
env:
TURBO_TELEMETRY_DISABLED: 1
The concurrency group cancels in-progress runs when a new push arrives for the same branch. See Concurrency & Caching for details.
quality
Name: Lint, Typecheck, Test, Build
Trigger: All pushes and PRs to main
Timeout: 15 minutes
Permissions: Default (read)
Service Containers
| Service | Image | Port | Credentials |
|---|---|---|---|
postgres | pgvector/pgvector:pg18 | 5433:5432 | trovella / trovella_dev |
redis | redis:8-alpine | 6379:6379 | None |
typesense | typesense/typesense:27.1 | 8108:8108 | API key: ci-test-key |
Environment Variables
| Variable | Value | Purpose |
|---|---|---|
DATABASE_URL | postgresql://trovella:trovella_dev@localhost:5433/trovella | CI Postgres (not production) |
REDIS_URL | redis://localhost:6379 | CI Redis |
BETTER_AUTH_SECRET | ci-test-secret-at-least-32-characters-long | Auth library requirement |
BETTER_AUTH_URL | http://localhost:3000 | Auth callback base |
GOOGLE_CLIENT_ID | ci-placeholder | OAuth (not exercised in tests) |
GOOGLE_CLIENT_SECRET | ci-placeholder | OAuth (not exercised in tests) |
ANTHROPIC_API_KEY | ci-placeholder | AI SDK (not exercised in tests) |
GOOGLE_AI_API_KEY | ci-placeholder | Google AI (not exercised in tests) |
TYPESENSE_API_KEY | ci-test-key | Search integration tests |
TYPESENSE_URL | http://localhost:8108 | Search integration tests |
Placeholder values prevent the application from failing at startup due to missing env vars. Tests that would call external APIs are mocked or skipped.
Steps (in order)
| # | Step | Command |
|---|---|---|
| 1 | Checkout | actions/checkout@v4 (fetch-depth: 2) |
| 2 | Install pnpm | pnpm/action-setup@v4 |
| 3 | Setup Node.js | actions/setup-node@v4 (reads .node-version, caches pnpm) |
| 4 | Install dependencies | pnpm install --frozen-lockfile |
| 5 | Check formatting | pnpm format:check |
| 6 | Lint | pnpm turbo lint --affected |
| 7 | Dependency graph | pnpm dep-cruise |
| 8 | Dead code detection | pnpm lint:dead-code |
| 9 | Duplication detection | pnpm lint:duplication |
| 10 | Upload duplication report | actions/upload-artifact@v4 (always, 14-day retention) |
| 11 | Typecheck | pnpm turbo typecheck --affected |
| 12 | Database migrations | pnpm db:migrate |
| 13 | Test | pnpm turbo test --affected |
| 14 | Build | pnpm turbo build --affected (with SENTRY_AUTH_TOKEN secret) |
The step order is deliberate. See Quality Checks for why lint must precede typecheck and why migrations must precede tests.
docs
Name: Documentation Quality
Trigger: All pushes and PRs to main
Timeout: 5 minutes
Permissions: Default (read)
Does not gate deployment.
Steps (in order)
| # | Step | Command |
|---|---|---|
| 1 | Checkout | actions/checkout@v4 (fetch-depth: 0, full history for freshness) |
| 2 | Install pnpm | pnpm/action-setup@v4 |
| 3 | Setup Node.js | actions/setup-node@v4 (reads .node-version, caches pnpm) |
| 4 | Install dependencies | pnpm install --frozen-lockfile |
| 5 | Sync Vale styles | pnpm vale sync |
| 6 | Vale prose lint | pnpm docs:lint |
| 7 | Link check | pnpm docs:links |
| 8 | Freshness check | pnpm docs:freshness (with CI=true) |
| 9 | Upload freshness report | actions/upload-artifact@v4 (always, 14-day retention) |
The checkout uses fetch-depth: 0 (full Git history) because the freshness check compares file modification dates against Git log timestamps.
The docs job was separated from quality so prose lint failures do not block production deploys. A typo in a guide is not worth holding back a security fix.
build-push
Name: Build & Push Docker Image
Trigger: Main branch only (push + refs/heads/main)
Timeout: 15 minutes
Depends on: None (runs in parallel with quality)
Permissions: contents: read, id-token: write (for WIF)
Environment Variables
| Variable | Value | Purpose |
|---|---|---|
REGISTRY | us-central1-docker.pkg.dev/trovella-shared/trovella | Artifact Registry path |
IMAGE | us-central1-docker.pkg.dev/trovella-shared/trovella/web | Full image path |
Steps (in order)
| # | Step | Details |
|---|---|---|
| 1 | Checkout | actions/checkout@v4 |
| 2 | Authenticate to GCP | WIF via google-github-actions/auth@v2 |
| 3 | Configure Docker | gcloud auth configure-docker us-central1-docker.pkg.dev |
| 4 | Read SENTRY_DSN | From GCP Secret Manager (trovella-sentry-dsn) |
| 5 | Setup Buildx | docker/setup-buildx-action@v3 |
| 6 | Build and push | docker/build-push-action@v6 |
Build Configuration
context: .
file: apps/web/Dockerfile
push: true
tags:
- $IMAGE:$SHA # immutable rollback target
- $IMAGE:latest # mutable, pulled by docker compose
cache-from: type=gha
cache-to: type=gha,mode=max
build-args:
- NEXT_PUBLIC_BETTER_AUTH_URL=https://trovella.ai
- NEXT_PUBLIC_SENTRY_DSN=$SENTRY_DSN
- SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN
NEXT_PUBLIC_* variables are inlined by Next.js at build time and cannot be overridden at runtime. The SENTRY_AUTH_TOKEN GitHub secret enables source map uploads during the build.
For the Docker multi-stage build details, see Infrastructure -- Deploy Pipeline.
migrate-prod
Name: Migrate Production Database
Trigger: Main branch only
Timeout: 5 minutes
Depends on: quality
Permissions: contents: read, id-token: write (for WIF)
Change Detection
Uses dorny/paths-filter@v3 to check whether the push includes changes in:
packages/db/src/migrations/**packages/db/src/schema/**packages/db/src/seed/**
If none of these paths changed, every subsequent step is skipped. This avoids the ~15 seconds of Cloud SQL Auth Proxy setup overhead on code-only deploys.
Steps (when migrations detected)
| # | Step | Details |
|---|---|---|
| 1 | Checkout | actions/checkout@v4 |
| 2 | Check for schema changes | dorny/paths-filter@v3 |
| 3 | Authenticate to GCP | WIF via google-github-actions/auth@v2 |
| 4 | Read DATABASE_URL | From Secret Manager, rewritten for local proxy |
| 5 | Install Cloud SQL Auth Proxy | Download v2.21.2 binary |
| 6 | Start proxy | Background process with health check (30s timeout) |
| 7 | Install pnpm | pnpm/action-setup@v4 |
| 8 | Setup Node.js | actions/setup-node@v4 |
| 9 | Install dependencies | pnpm install --frozen-lockfile |
| 10 | Run migrations | pnpm db:migrate |
| 11 | Verify migration count | Compare _journal.json entries to drizzle.__drizzle_migrations rows |
| 12 | Seed reference data | pnpm db:seed with NODE_ENV=production |
| 13 | Dump proxy logs | Always runs (even on success) |
For the full migration mechanics (proxy configuration, readiness checks, URL rewriting, verification), see Data & Storage -- CI Deployment.
deploy-prod
Name: Deploy to Production VM
Trigger: Main branch only
Timeout: 10 minutes
Depends on: quality, build-push, migrate-prod
Permissions: contents: read, id-token: write (for WIF)
Environment Variables
| Variable | Value | Purpose |
|---|---|---|
VM_NAME | trovella-prod-vm | Target VM |
VM_ZONE | us-central1-a | GCP zone |
VM_PROJECT | trovella-prod | GCP project |
Steps (in order)
| # | Step | Details |
|---|---|---|
| 1 | Checkout | actions/checkout@v4 |
| 2 | Authenticate to GCP | WIF via google-github-actions/auth@v2 |
| 3 | Copy compose files | SCP via IAP: docker-compose.prod.yml, Caddyfile, sync-secrets-vm.sh |
| 4 | Sync secrets and deploy | SSH via IAP: move files, sync secrets, pull image, restart containers, prune |
The deploy is a single SSH command that runs five operations sequentially on the VM:
- Move uploaded files to
/opt/trovella/ - Run
sync-secrets-vm.sh(Secret Manager to.env) gcloud auth configure-docker(for Artifact Registry pull)docker compose pull(fetches changed images only)docker compose up -d --remove-orphans+docker image prune -f
For the full deploy mechanics (container dependencies, health checks, secret sync), see Infrastructure -- Deploy Pipeline.