`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) <noreply@anthropic.com>
This commit is contained in:
@@ -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(/<script>([\s\S]*?)<\/script>/);
|
||||
if(!m){ console.error('No inline script found in index.html'); process.exit(1); }
|
||||
const hash='sha256-'+crypto.createHash('sha256').update(m[1]).digest('base64');
|
||||
const expected='sha256-1tTDwCq+TCEyPDSZeYqW5HbmP+unUg8hrgRiZBiH/IU=';
|
||||
if(hash!==expected){
|
||||
console.error('Inline script hash changed.');
|
||||
console.error(' now: '+hash);
|
||||
console.error(' expected: '+expected);
|
||||
console.error('Update the CSP in deploy/Caddyfile.example AND on the server,');
|
||||
console.error('then update the expected hash in this workflow.');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('CSP hash unchanged: '+hash);
|
||||
"
|
||||
|
||||
- name: Docker image builds
|
||||
run: docker build -t pig:ci .
|
||||
Reference in New Issue
Block a user