Files
pig/.gitea/workflows/ci.yml
2026-08-13 01:39:01 -07:00

164 lines
6.6 KiB
YAML

# 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:`, because
# this runner is configured with `container.network: host`.
#
# That single setting explains three failed attempts, and is worth writing
# down so nobody repeats them:
#
# `services:` Service containers are not resolvable by
# name from a host-networked job, giving
# "getaddrinfo EAI_AGAIN postgres".
# `--network container:$HOSTNAME` /etc/hostname reports the HOST's name,
# not a container id, so the namespace
# join finds no such container.
# default-gateway addressing The wrong idea entirely: with host
# networking the default route is the
# real router, not a docker bridge.
#
# Because the job shares the host's network namespace, a published port is
# simply on 127.0.0.1. The port is derived from the run id so concurrent
# runs cannot collide.
env:
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: |
PG_PORT=$(( 45000 + (${{ github.run_id }} % 15000) ))
echo "Publishing Postgres on 127.0.0.1:${PG_PORT}"
docker rm -f "$PG_CONTAINER" 2>/dev/null || true
docker run -d --name "$PG_CONTAINER" \
-p "127.0.0.1:${PG_PORT}:5432" \
-e POSTGRES_USER=pig -e POSTGRES_PASSWORD=pig -e POSTGRES_DB=pig \
postgres:16-alpine
# pg_isready inside the container only proves the server started.
# What matters is that THIS job can reach it through the published
# port, so the readiness check is made from here, over TCP.
for i in $(seq 1 60); do
if node -e "
const net=require('net');
const s=net.connect(${PG_PORT},'127.0.0.1');
s.on('connect',()=>{s.end();process.exit(0)});
s.on('error',()=>process.exit(1));
" 2>/dev/null; then
echo "Reachable after ${i}s"
echo "DATABASE_URL=postgres://pig:pig@127.0.0.1:${PG_PORT}/pig" >> "$GITHUB_ENV"
exit 0
fi
sleep 1
done
echo "Postgres never became reachable on 127.0.0.1:${PG_PORT}"
docker logs "$PG_CONTAINER" 2>&1 | tail -30
exit 1
- name: Install
run: npm install --no-audit --no-fund
- name: Typecheck every package
run: npm run typecheck
- 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: Critical path E2E against Postgres and Hono
run: npm run test:e2e
- 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 .
- name: Stop Postgres
if: always()
run: docker rm -f "$PG_CONTAINER" 2>/dev/null || true