From 53ef6d3d9f5f59a13e1c845cff3d19f918e66bc5 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Fri, 2 Oct 2026 06:40:56 +0000 Subject: [PATCH] Ship the Postgres migrations in the image, not the SQLite ones The Dockerfile copied packages/storage/migrations (SQLite) to /api/migrations and start.sh ran them against the production Postgres. Every file was skipped only because its name matched a migrations-pg row in schema_migrations; the next new migration would have run SQLite SQL against prod. - Dockerfile copies migrations-pg to /api/migrations-pg and fails the build if any file lacks the "-- TronBrowser Postgres schema:" header. - start.sh points MIGRATIONS_DIR at /api/migrations-pg. - db-migrate.mjs logs the folder it reads and refuses, before touching the database, a Postgres target whose folder holds non-Postgres migrations. - CI asserts that refusal on the SQLite folder. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 4 ++++ Dockerfile | 9 ++++++++- scripts/db-migrate.mjs | 13 +++++++++++++ start.sh | 2 +- 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index caa533c..d7a10ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -85,6 +85,10 @@ jobs: - name: Migrate + boot the API on Bun run: | set -e + # The runner must refuse the legacy SQLite migrations on a Postgres target. + if MIGRATIONS_DIR=packages/storage/migrations bun scripts/db-migrate.mjs; then + echo "db-migrate applied SQLite migrations to Postgres" >&2; exit 1 + fi MIGRATIONS_DIR=packages/storage/migrations-pg bun scripts/db-migrate.mjs PORT=8090 bun services/api/dist/index.js > api.log 2>&1 & for i in $(seq 1 30); do curl -fsS http://127.0.0.1:8090/api/healthz && break; sleep 1; done diff --git a/Dockerfile b/Dockerfile index 1901ead..a7d0c41 100644 --- a/Dockerfile +++ b/Dockerfile @@ -84,7 +84,14 @@ COPY --from=api /out/node_modules /api/node_modules COPY --from=api /out/package.json /api/package.json # DB migrations run on boot (start.sh) so schema never drifts from the deploy. COPY scripts/db-migrate.mjs /api/db-migrate.mjs -COPY packages/storage/migrations /api/migrations +# The API runs on Postgres, so the image carries the Postgres migrations +# (migrations-pg), never the legacy SQLite ones in packages/storage/migrations. +# Fail the build if any shipped file lacks the Postgres-schema header. +COPY packages/storage/migrations-pg /api/migrations-pg +RUN for f in /api/migrations-pg/*.sql; do \ + head -1 "$f" | grep -q '^-- TronBrowser Postgres schema:' \ + || { echo "not a Postgres migration: $f" >&2; exit 1; }; \ + done COPY start.sh /start.sh RUN chmod +x /start.sh CMD ["/start.sh"] diff --git a/scripts/db-migrate.mjs b/scripts/db-migrate.mjs index 5abd7a6..e70cb16 100644 --- a/scripts/db-migrate.mjs +++ b/scripts/db-migrate.mjs @@ -31,6 +31,19 @@ if (!url && !filePath) { const DIR = process.env.MIGRATIONS_DIR || join(ROOT, isPostgres ? 'packages/storage/migrations-pg' : 'packages/storage/migrations'); const files = readdirSync(DIR).filter((f) => f.endsWith('.sql')).sort(); +console.log(`[migrate] ${isPostgres ? 'postgres' : 'libsql'} target, reading ${DIR} (${files.length} file(s))`); + +// Every Postgres migration starts with this header. A file without it is the +// legacy SQLite schema (packages/storage/migrations), which must never be run +// against the production Postgres; refuse before touching the database. +const PG_HEADER = /^-- TronBrowser Postgres schema:/; +if (isPostgres) { + const bad = files.filter((f) => !PG_HEADER.test(readFileSync(join(DIR, f), 'utf8'))); + if (bad.length) { + console.error(`[migrate] refusing: ${DIR} holds non-Postgres migrations (${bad.join(', ')}). Point MIGRATIONS_DIR at packages/storage/migrations-pg.`); + process.exit(1); + } +} if (isPostgres) { const { createClient } = await import('@profullstack/libsql-pg'); diff --git a/start.sh b/start.sh index 6ae4d7d..b6f01e1 100644 --- a/start.sh +++ b/start.sh @@ -4,7 +4,7 @@ set -e # Apply any pending DB migrations on boot (idempotent, forward-only). Non-fatal: # a transient DB hiccup shouldn't block the whole service from starting. -MIGRATIONS_DIR=/api/migrations bun /api/db-migrate.mjs || echo "[migrate] FAILED — continuing" +MIGRATIONS_DIR=/api/migrations-pg bun /api/db-migrate.mjs || echo "[migrate] FAILED — continuing" PORT=8090 bun /api/dist/index.js & # --- Tor v3 hidden service ---------------------------------------------------