Files
pig/.gitea/workflows/ci.yml
T
karti e6b4c1618e
CI / verify (push) Failing after 5s
CI: reach Postgres through the gateway instead of a shared namespace
Second attempt failed differently: /etc/hostname inside the job reports the
HOST's name rather than the container id, so `--network container:$HOSTNAME`
found no such container.

Rather than hunt for our own container id through /proc, publish the port on
the host and connect through the job container's default gateway. That needs
no container identity at all. The port is derived from the run id so two
concurrent runs cannot collide, and DATABASE_URL is exported through GITHUB_ENV
once Postgres is actually accepting connections.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 20:30:24 -07:00

159 lines
6.4 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:`.
#
# Two approaches were tried and rejected before this one:
#
# `services:` — this runner does not attach service
# containers to the job network, giving
# "getaddrinfo EAI_AGAIN postgres".
# `--network container:$HOSTNAME` — /etc/hostname inside the job reports
# the HOST's name, not the container id,
# so the namespace join finds no such
# container.
#
# What works without needing to know our own container id: publish the port
# on the host and reach it through the job container's default gateway. 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: |
# A per-run port in the ephemeral range, so two runs never collide.
PG_PORT=$(( 45000 + (${{ github.run_id }} % 15000) ))
GATEWAY=$(ip route | awk '/^default/ {print $3; exit}')
if [ -z "$GATEWAY" ]; then echo "Could not determine the gateway"; exit 1; fi
echo "Postgres will be published on ${GATEWAY}:${PG_PORT}"
docker rm -f "$PG_CONTAINER" 2>/dev/null || true
docker run -d --name "$PG_CONTAINER" \
-p "${PG_PORT}:5432" \
-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"
# Export for every later step.
echo "DATABASE_URL=postgres://pig:pig@${GATEWAY}:${PG_PORT}/pig" >> "$GITHUB_ENV"
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(/<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