Commit fac5a7a342

fac5a7a342e94f98df79ec81632b7d799395daa1

parent: 5da496fdbf

Unsigned

cmc <hello@cleberg.net> · 2026-08-21 04:37 UTC

Add CI and a production migration-drift check

The repo had no workflows at all: typecheck and the control-mapping sync
check existed as npm scripts but ran nowhere, so nothing gated a PR.

CI runs both on pull_request and on push to main.

The drift check exists because deploy and migrate are separate actions, so
production can serve code whose schema was never applied. That is not
hypothetical — 0008 shipped its code while its migration sat unapplied, and
per-alert evidence collapsed to one row per repo until someone noticed. CI
cannot catch it, having no view of the production database, so this runs on
a schedule against the real one and fails when migrations/ is ahead.

Dependabot now tracks github-actions so these do not rot.

Layout: unified · split

.github/dependabot.yml +5
@@ -9,3 +9,8 @@ updates:
9 directory: "/" # Location of package manifests 9 directory: "/" # Location of package manifests
10 schedule: 10 schedule:
11 interval: "weekly" 11 interval: "weekly"
12
13 - package-ecosystem: "github-actions"
14 directory: "/"
15 schedule:
16 interval: "weekly"
.github/workflows/ci.yml added +33
@@ -0,0 +1,33 @@
1name: CI
2
3on:
4 pull_request:
5 push:
6 branches: [main]
7
8permissions:
9 contents: read
10
11jobs:
12 check:
13 runs-on: ubuntu-latest
14 steps:
15 - uses: actions/checkout@v7
16
17 - uses: actions/setup-node@v7
18 with:
19 node-version: '26'
20 cache: npm
21
22 - run: npm ci
23
24 # The engine is TypeScript on Workers types; a type error is a deploy
25 # that fails in Cloudflare's build rather than here.
26 - name: Typecheck
27 run: npm run typecheck
28
29 # Applies every migration to an in-memory SQLite database and diffs the
30 # resulting control_mappings against docs/framework-mapping.md. The doc
31 # is what an auditor reads, so drift there is a lie in every export.
32 - name: Control mappings match the docs
33 run: npm run test:mappings
.github/workflows/migration-drift.yml added +45
@@ -0,0 +1,45 @@
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 - run: npm ci
29
30 - name: Every migration in migrations/ is applied to production
31 env:
32 CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
33 run: |
34 if [ -z "$CLOUDFLARE_API_TOKEN" ]; then
35 echo "::error::CLOUDFLARE_API_TOKEN is not set — this check cannot see production."
36 exit 1
37 fi
38 out=$(npx wrangler d1 migrations list DB --remote 2>&1) || true
39 echo "$out"
40 if echo "$out" | grep -q "No migrations to apply"; then
41 echo "Production schema matches migrations/."
42 else
43 echo "::error::Production is missing migrations listed above. Run 'npm run db:migrate:remote'."
44 exit 1
45 fi