Commit d500efb2bb
d500efb2bba32bbe16945f088b37a135706a77f0
parent: 7895806478
Unsigned
cmc <hello@cleberg.net> · 2026-08-22 19:46 UTC
Trim CI to the mappings check, guard migrations in the Cloudflare build
Workers Builds is connected to this repo and already runs
'wrangler types && tsc --noEmit' as its build command, on every branch, so the
GitHub Actions typecheck was a second copy of the same signal. What it does not
run is check-mappings, which needs no Cloudflare credentials — so that stays
here, and now runs without an install step because the script has no
dependencies.
migration-drift.yml is removed. It needed a CLOUDFLARE_API_TOKEN secret that was
never set, so it would have gone red on its daily cron reporting nothing. The
check belongs in the build that is about to deploy, not on a timer a day later:
Workers Builds is already authenticated and runs immediately before
'wrangler deploy'.
check:migrations is the guard, kept in package.json rather than pasted into the
dashboard so it is reviewable and versioned. Set the Workers Builds build
command to:
npm run check:migrations && npx wrangler types && npx tsc --noEmit
A build that would put code ahead of its schema then fails instead of deploying,
which is the 0008 case caught at the moment it matters.
Layout: unified · split
.github/workflows/ci.yml
+13 −21
| @@ -9,7 +9,14 @@ permissions: |
| 9 | 9 | contents: read |
| 10 | 10 | |
| 11 | 11 | jobs: |
| 12 | | check: |
| 12 | # Only the mapping check lives here. Cloudflare Workers Builds already runs |
| 13 | # `wrangler types && tsc --noEmit` as its build command, on every branch, so |
| 14 | # duplicating the typecheck bought a second copy of the same signal. |
| 15 | # |
| 16 | # What Workers Builds does not do is compare the control mappings against the |
| 17 | # docs — and that check needs no Cloudflare credentials, which is why it can |
| 18 | # live here and the migration guard cannot. |
| 19 | mappings: |
| 13 | 20 | runs-on: ubuntu-latest |
| 14 | 21 | steps: |
| 15 | 22 | - uses: actions/checkout@v7 |
| @@ -17,25 +24,10 @@ jobs: |
| 17 | 24 | - uses: actions/setup-node@v7 |
| 18 | 25 | with: |
| 19 | 26 | node-version: '26' |
| 20 | | cache: npm |
| 21 | 27 | |
| 22 | | # --ignore-scripts: no dependency here needs a lifecycle hook, and CI |
| 23 | | # should not run arbitrary postinstall code from the tree. |
| 24 | | - run: npm ci --ignore-scripts |
| 25 | | |
| 26 | | # worker-configuration.d.ts is generated, not committed, and tsconfig |
| 27 | | # lists it under "types" — so tsc cannot run on a fresh checkout without |
| 28 | | # this. Needs no Cloudflare credentials; it reads wrangler.jsonc. |
| 29 | | - name: Generate Workers types |
| 30 | | run: npm run types |
| 31 | | |
| 32 | | # The engine is TypeScript on Workers types; a type error is a deploy |
| 33 | | # that fails in Cloudflare's build rather than here. |
| 34 | | - name: Typecheck |
| 35 | | run: npm run typecheck |
| 36 | | |
| 37 | | # Applies every migration to an in-memory SQLite database and diffs the |
| 38 | | # resulting control_mappings against docs/framework-mapping.md. The doc |
| 39 | | # is what an auditor reads, so drift there is a lie in every export. |
| 28 | # No install step: check-mappings.mjs has no dependencies. It applies the |
| 29 | # migrations to an in-memory SQLite database using Node's built-in module |
| 30 | # and diffs the resulting control_mappings against |
| 31 | # docs/framework-mapping.md, rationale text included. |
| 40 | 32 | - name: Control mappings match the docs |
| 41 | | run: npm run test:mappings |
| 33 | run: node scripts/check-mappings.mjs |
.github/workflows/migration-drift.yml
deleted
−47
| @@ -1,47 +0,0 @@ |
| 1 | | name: Migration drift |
| 2 | | |
| 3 | | # Deploys and migrations are separate actions, so production can run code whose |
| 4 | | # schema was never applied — which is exactly what happened with 0008: its code |
| 5 | | # shipped, its migration did not, and evidence output was silently wrong until |
| 6 | | # someone went looking. Nothing in CI can catch that, because CI has no view of |
| 7 | | # the production database. This does. |
| 8 | | |
| 9 | | on: |
| 10 | | schedule: |
| 11 | | - cron: '0 9 * * *' |
| 12 | | workflow_dispatch: |
| 13 | | |
| 14 | | permissions: |
| 15 | | contents: read |
| 16 | | |
| 17 | | jobs: |
| 18 | | drift: |
| 19 | | runs-on: ubuntu-latest |
| 20 | | steps: |
| 21 | | - uses: actions/checkout@v7 |
| 22 | | |
| 23 | | - uses: actions/setup-node@v7 |
| 24 | | with: |
| 25 | | node-version: '26' |
| 26 | | cache: npm |
| 27 | | |
| 28 | | # --ignore-scripts: no dependency here needs a lifecycle hook, and CI |
| 29 | | # should not run arbitrary postinstall code from the tree. |
| 30 | | - run: npm ci --ignore-scripts |
| 31 | | |
| 32 | | - name: Every migration in migrations/ is applied to production |
| 33 | | env: |
| 34 | | CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} |
| 35 | | run: | |
| 36 | | if [ -z "$CLOUDFLARE_API_TOKEN" ]; then |
| 37 | | echo "::error::CLOUDFLARE_API_TOKEN is not set — this check cannot see production." |
| 38 | | exit 1 |
| 39 | | fi |
| 40 | | out=$(./node_modules/.bin/wrangler d1 migrations list DB --remote 2>&1) || true |
| 41 | | echo "$out" |
| 42 | | if echo "$out" | grep -q "No migrations to apply"; then |
| 43 | | echo "Production schema matches migrations/." |
| 44 | | else |
| 45 | | echo "::error::Production is missing migrations listed above. Run 'npm run db:migrate:remote'." |
| 46 | | exit 1 |
| 47 | | fi |
package.json
+2 −1
| @@ -10,7 +10,8 @@ |
| 10 | 10 | "typecheck": "tsc --noEmit", |
| 11 | 11 | "test:mappings": "node scripts/check-mappings.mjs", |
| 12 | 12 | "db:migrate:local": "wrangler d1 migrations apply DB --local", |
| 13 | | "db:migrate:remote": "wrangler d1 migrations apply DB --remote" |
| 13 | "db:migrate:remote": "wrangler d1 migrations apply DB --remote", |
| 14 | "check:migrations": "wrangler d1 migrations list DB --remote | grep -q 'No migrations to apply' || { echo 'Pending D1 migrations. Run: npm run db:migrate:remote' >&2; exit 1; }" |
| 14 | 15 | }, |
| 15 | 16 | "devDependencies": { |
| 16 | 17 | "@types/node": "^26.2.0", |