From 73231a8944fb829ff53a2d22549e1e2b07120648 Mon Sep 17 00:00:00 2001 From: karti Date: Wed, 12 Aug 2026 20:27:47 -0700 Subject: [PATCH] Add CI, a test suite, and a deploy script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `npm test` did nothing until now. CI that runs no tests is theatre, so the tests came first — 39 of them, over the two places where an error would be silent and expensive. packages/core: the margin arithmetic. Every dashboard figure, idle-capacity alert and agent answer resolves through it, and wrong numbers still look like numbers. The cases pin decisions rather than implementation: cost is charged against the full commitment (a naive version reports the opposite sign on a loss-making block), aggregation sums cents rather than averaging percentages (averaging reports +22% on a book that is losing money), break-even prices the remaining hours and returns null rather than Infinity when there are none, and internal research burn counts as cost with no revenue. packages/prime: the upstream mapping. Rounding rather than truncating cents, because 2.43 is 2.4299999 in binary and a lost cent compounds across millions of GPU-hours. And interconnect normalisation, where an unrecognised fabric maps to Unknown rather than Ethernet — guessing low loses a deal, guessing high sells a training customer a cluster that cannot train. CI runs on push and pull request: typecheck all six packages, unit tests, migrations applied twice to a real Postgres, a seed-idempotency assertion that fails the build if row counts move on a second run, a server boot, the front-end build, and a Docker build. It also asserts the inline theme script's hash still matches the CSP the proxy allows. That script prevents a white flash for dark-mode users; if it changes without the CSP being updated, the browser silently blocks it and nothing anywhere reports an error. Deployment stays a script rather than push-to-deploy. Automating it would put an SSH key with production write access on the CI runner — a real escalation for a project this size. The script takes a database dump before migrating and refuses to finish if an unauthenticated request returns anything but 401. Co-Authored-By: Claude Opus 5 (1M context) --- .gitea/workflows/ci.yml | 124 ++++++++++++++++++++ .gitignore | 1 + packages/core/package.json | 3 +- packages/core/test/margin.test.ts | 184 ++++++++++++++++++++++++++++++ packages/core/tsconfig.json | 8 +- packages/prime/package.json | 13 ++- packages/prime/test/map.test.ts | 136 ++++++++++++++++++++++ packages/prime/tsconfig.json | 4 +- scripts/deploy.sh | 69 +++++++++++ 9 files changed, 530 insertions(+), 12 deletions(-) create mode 100644 .gitea/workflows/ci.yml create mode 100644 packages/core/test/margin.test.ts create mode 100644 packages/prime/test/map.test.ts create mode 100755 scripts/deploy.sh diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..909ba11 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,124 @@ +# 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 + + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_USER: pig + POSTGRES_PASSWORD: pig + POSTGRES_DB: pig + options: >- + --health-cmd "pg_isready -U pig" + --health-interval 5s + --health-timeout 5s + --health-retries 10 + + env: + DATABASE_URL: postgres://pig:pig@postgres:5432/pig + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '22' + + - 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 + BEFORE=$(psql "$DATABASE_URL" -tAc "select count(*) from contacts") + npx tsx packages/db/src/seed/index.ts > /dev/null + AFTER=$(psql "$DATABASE_URL" -tAc "select count(*) from contacts") + 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(/