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:
99 contents: read
1010
1111jobs:
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:
1320 runs-on: ubuntu-latest
1421 steps:
1522 - uses: actions/checkout@v7
@@ -17,25 +24,10 @@ jobs:
1724 - uses: actions/setup-node@v7
1825 with:
1926 node-version: '26'
20 cache: npm
2127
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.
4032 - 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 @@
1name: 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
9on:
10 schedule:
11 - cron: '0 9 * * *'
12 workflow_dispatch:
13
14permissions:
15 contents: read
16
17jobs:
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 @@
1010 "typecheck": "tsc --noEmit",
1111 "test:mappings": "node scripts/check-mappings.mjs",
1212 "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; }"
1415 },
1516 "devDependencies": {
1617 "@types/node": "^26.2.0",