audit-labs/gh-attest

GitHub Audit Evidence Extractor

clone: git clone https://gitbay.org/audit-labs/gh-attest.git

0849131c0bd24eafbcf704c4fd15d33fdebb643e

verified · cmc

author: Christian Cleberg <hello@cleberg.net> · 2026-08-22T19:46:11Z

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.
 .github/workflows/ci.yml              | 34 ++++++++++---------------
 .github/workflows/migration-drift.yml | 47 -----------------------------------
 package.json                          |  3 ++-
 3 files changed, 15 insertions(+), 69 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index fe35618..e8c776f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -9,7 +9,14 @@ permissions:
   contents: read
 
 jobs:
-  check:
+  # Only the mapping check lives here. Cloudflare Workers Builds already runs
+  # `wrangler types && tsc --noEmit` as its build command, on every branch, so
+  # duplicating the typecheck bought a second copy of the same signal.
+  #
+  # What Workers Builds does not do is compare the control mappings against the
+  # docs — and that check needs no Cloudflare credentials, which is why it can
+  # live here and the migration guard cannot.
+  mappings:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v7
@@ -17,25 +24,10 @@ jobs:
       - uses: actions/setup-node@v7
         with:
           node-version: '26'
-          cache: npm
 
-      # --ignore-scripts: no dependency here needs a lifecycle hook, and CI
-      # should not run arbitrary postinstall code from the tree.
-      - run: npm ci --ignore-scripts
-
-      # worker-configuration.d.ts is generated, not committed, and tsconfig
-      # lists it under "types" — so tsc cannot run on a fresh checkout without
-      # this. Needs no Cloudflare credentials; it reads wrangler.jsonc.
-      - name: Generate Workers types
-        run: npm run types
-
-      # The engine is TypeScript on Workers types; a type error is a deploy
-      # that fails in Cloudflare's build rather than here.
-      - name: Typecheck
-        run: npm run typecheck
-
-      # Applies every migration to an in-memory SQLite database and diffs the
-      # resulting control_mappings against docs/framework-mapping.md. The doc
-      # is what an auditor reads, so drift there is a lie in every export.
+      # No install step: check-mappings.mjs has no dependencies. It applies the
+      # migrations to an in-memory SQLite database using Node's built-in module
+      # and diffs the resulting control_mappings against
+      # docs/framework-mapping.md, rationale text included.
       - name: Control mappings match the docs
-        run: npm run test:mappings
+        run: node scripts/check-mappings.mjs
diff --git a/.github/workflows/migration-drift.yml b/.github/workflows/migration-drift.yml
deleted file mode 100644
index 2e64cf3..0000000
--- a/.github/workflows/migration-drift.yml
+++ /dev/null
@@ -1,47 +0,0 @@
-name: Migration drift
-
-# Deploys and migrations are separate actions, so production can run code whose
-# schema was never applied — which is exactly what happened with 0008: its code
-# shipped, its migration did not, and evidence output was silently wrong until
-# someone went looking. Nothing in CI can catch that, because CI has no view of
-# the production database. This does.
-
-on:
-  schedule:
-    - cron: '0 9 * * *'
-  workflow_dispatch:
-
-permissions:
-  contents: read
-
-jobs:
-  drift:
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v7
-
-      - uses: actions/setup-node@v7
-        with:
-          node-version: '26'
-          cache: npm
-
-      # --ignore-scripts: no dependency here needs a lifecycle hook, and CI
-      # should not run arbitrary postinstall code from the tree.
-      - run: npm ci --ignore-scripts
-
-      - name: Every migration in migrations/ is applied to production
-        env:
-          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
-        run: |
-          if [ -z "$CLOUDFLARE_API_TOKEN" ]; then
-            echo "::error::CLOUDFLARE_API_TOKEN is not set — this check cannot see production."
-            exit 1
-          fi
-          out=$(./node_modules/.bin/wrangler d1 migrations list DB --remote 2>&1) || true
-          echo "$out"
-          if echo "$out" | grep -q "No migrations to apply"; then
-            echo "Production schema matches migrations/."
-          else
-            echo "::error::Production is missing migrations listed above. Run 'npm run db:migrate:remote'."
-            exit 1
-          fi
diff --git a/package.json b/package.json
index d03c55b..c4425fd 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,8 @@
     "typecheck": "tsc --noEmit",
     "test:mappings": "node scripts/check-mappings.mjs",
     "db:migrate:local": "wrangler d1 migrations apply DB --local",
-    "db:migrate:remote": "wrangler d1 migrations apply DB --remote"
+    "db:migrate:remote": "wrangler d1 migrations apply DB --remote",
+    "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; }"
   },
   "devDependencies": {
     "@types/node": "^26.2.0",