# Continuous integration. # # Runs on every push and pull request. The job is deliberately one sequence # rather than a fan-out: this is a small project, the whole thing takes a # couple of minutes, and a single log is easier to read than five. # # What it actually proves, in order of how likely each is to catch something: # # 1. Every package typechecks. # 2. The migration chain applies to a REAL, empty Postgres. This has already # caught one migration that Drizzle generated but Postgres refused # (a jsonb -> integer cast with no USING clause). # 3. The seed is idempotent — running it twice leaves the same row counts. # This caught a seed that silently duplicated 27 contacts. # 4. The unit tests pass. # 5. The server boots against that database and answers. # 6. The front end builds, and the CSP hash for the inline theme script still # matches what the proxy is configured to allow. Editing that script # changes its hash, and the failure mode is a silent white flash for # dark-mode users rather than an error. name: CI on: push: branches: [main] pull_request: jobs: verify: runs-on: ubuntu-latest # Postgres is started as a step rather than through `services:`. # # The runner here does not put service containers on the job's network, so # `services:` yields "getaddrinfo EAI_AGAIN postgres". Sharing the job # container's own network namespace (`--network container:$HOSTNAME`) puts # Postgres on 127.0.0.1 and works regardless of how the runner is # configured — which matters because this runner is shared with other # repositories and should not have to be reconfigured to suit this one. env: DATABASE_URL: postgres://pig:pig@127.0.0.1:5432/pig PG_CONTAINER: pig-ci-pg-${{ github.run_id }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '22' - name: Start Postgres run: | docker rm -f "$PG_CONTAINER" 2>/dev/null || true docker run -d --name "$PG_CONTAINER" \ --network "container:$(cat /etc/hostname)" \ -e POSTGRES_USER=pig -e POSTGRES_PASSWORD=pig -e POSTGRES_DB=pig \ postgres:16-alpine for i in $(seq 1 60); do if docker exec "$PG_CONTAINER" pg_isready -U pig -q; then echo "Postgres ready after ${i}s"; exit 0 fi sleep 1 done echo "Postgres did not become ready" docker logs "$PG_CONTAINER" | tail -30 exit 1 - name: Install run: npm install --no-audit --no-fund - name: Typecheck every package run: | npx tsc --noEmit -p packages/core/tsconfig.json npx tsc --noEmit -p packages/db/tsconfig.json npx tsc --noEmit -p packages/prime/tsconfig.json npx tsc --noEmit -p apps/api/tsconfig.json npx tsc --noEmit -p apps/web/tsconfig.json npx tsc --noEmit -p apps/mcp/tsconfig.json - name: Unit tests run: npm test --workspaces --if-present - name: Migrations apply to a real Postgres run: npx tsx packages/db/src/migrate.ts - name: Migrations are re-runnable run: npx tsx packages/db/src/migrate.ts - name: Seed is idempotent # A seed that duplicates on a second run corrupts any database it is # pointed at twice, and nobody notices until the counts look odd. run: | npx tsx packages/db/src/seed/index.ts > /dev/null count() { docker exec "$PG_CONTAINER" psql -U pig -d pig -tAc "select count(*) from contacts"; } BEFORE=$(count) npx tsx packages/db/src/seed/index.ts > /dev/null AFTER=$(count) echo "contacts: $BEFORE -> $AFTER" test "$BEFORE" = "$AFTER" || { echo "SEED IS NOT IDEMPOTENT"; exit 1; } - name: Server boots and answers run: | NODE_ENV=development PIG_PORT=8930 npx tsx apps/api/src/server.ts & for i in $(seq 1 30); do curl -sf http://127.0.0.1:8930/api/health && break sleep 1 done curl -sf http://127.0.0.1:8930/api/health | grep -q '"ok":true' - name: Front end builds run: npm run build -w @pig/web - name: Inline theme script still matches the deployed CSP hash # The proxy allows exactly one inline script by hash. If the script # changes and the CSP is not updated, dark-mode users get a white flash # on every load and nothing anywhere reports an error. run: | node -e " const fs=require('fs'), crypto=require('crypto'); const html=fs.readFileSync('apps/web/dist/index.html','utf8'); const m=html.match(/