audit-labs/gh-attest

GitHub Audit Evidence Extractor

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

7a25bf40f853eede7bb10c00eede68553636e3d3

verified · cmc

author: Christian Cleberg <hello@cleberg.net> · 2026-08-21T04:37:05Z

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.
 .github/dependabot.yml                |  5 ++++
 .github/workflows/ci.yml              | 33 +++++++++++++++++++++++++
 .github/workflows/migration-drift.yml | 45 +++++++++++++++++++++++++++++++++++
 3 files changed, 83 insertions(+)

diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 5f0889c..9d2b19f 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -9,3 +9,8 @@ updates:
     directory: "/" # Location of package manifests
     schedule:
       interval: "weekly"
+
+  - package-ecosystem: "github-actions"
+    directory: "/"
+    schedule:
+      interval: "weekly"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..8952e0b
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,33 @@
+name: CI
+
+on:
+  pull_request:
+  push:
+    branches: [main]
+
+permissions:
+  contents: read
+
+jobs:
+  check:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v7
+
+      - uses: actions/setup-node@v7
+        with:
+          node-version: '26'
+          cache: npm
+
+      - run: npm ci
+
+      # 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.
+      - name: Control mappings match the docs
+        run: npm run test:mappings
diff --git a/.github/workflows/migration-drift.yml b/.github/workflows/migration-drift.yml
new file mode 100644
index 0000000..3372b5b
--- /dev/null
+++ b/.github/workflows/migration-drift.yml
@@ -0,0 +1,45 @@
+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
+
+      - run: npm ci
+
+      - 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=$(npx 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