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 @@ |
| |
1 | name: CI |
| |
2 | |
| |
3 | on: |
| |
4 | pull_request: |
| |
5 | push: |
| |
6 | branches: [main] |
| |
7 | |
| |
8 | permissions: |
| |
9 | contents: read |
| |
10 | |
| |
11 | jobs: |
| |
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 @@ |
| |
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 | - 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 |