emdashcms.org deploys to Cloudflare Workers via a GitHub Actions pipeline. The entire pipeline lives in .github/workflows/deploy.yml — there is no Cloudflare dashboard build configuration to chase down.
Every push to main triggers the Deploy workflow. It runs:
- Checkout + Node 22 install + npm ci — clean dependency tree.
npm test— vitest suite. If anything fails, the pipeline halts here.npm run build— runswrangler types && astro check && astro build. Type errors and astro check failures halt the pipeline.wrangler d1 migrations list emdashcms-org --remote— visibility step. Prints the applied/pending table into the workflow log so anyone reviewing the run can see exactly what's about to be applied to production.wrangler d1 migrations apply emdashcms-org --remote— the migration gate. If migrations fail (drift, syntax error, duplicate columns) the pipeline halts here. The previous worker keeps serving the previous schema. Seedocs/database.mdfor recovery.wrangler deploy— uploads the built worker. Cloudflare flips traffic to the new version atomically.
A separate CI workflow runs the same validation (tests + build) on every pull request and on every push to a non-main branch. It does not need any secrets, so contributors can fork the repo and validate their changes without configuring anything.
- The deploy workflow uses a
deploy-productionconcurrency group withcancel-in-progress: false. Two simultaneous pushes will queue rather than cancel each other — important because the migration step is not safe to interrupt mid-way. - The CI workflow uses
cancel-in-progress: truebecause validating an outdated commit is a waste. - Production secrets (
GITHUB_CLIENT_SECRET,JWT_SECRET,TURNSTILE_SECRET_KEY) live on the worker itself viawrangler secret put. They are not in GitHub Actions and the deploy step never sees them.
These three steps are not in the repo because they involve credentials and external service configuration. They only need to happen once.
In the Cloudflare dashboard, create a token with these permissions on the account that hosts emdashcms.org:
- Account → Workers Scripts → Edit — required to deploy the worker
- Account → D1 → Edit — required to apply migrations
- Account → Workers KV Storage → Edit — required to deploy bindings to KV namespaces
- Account → Workers R2 Storage → Edit — required to deploy bindings to R2 buckets
- Account → Account Settings → Read — required for account discovery
- User → User Details → Read — required for
wrangler whoamichecks during deploy
Scope the token to the specific account hosting the worker. Set no expiry, or rotate annually if you want a recurring chore.
In the GitHub repo at Settings → Secrets and variables → Actions → New repository secret, add:
CLOUDFLARE_API_TOKEN— the token from step 1CLOUDFLARE_ACCOUNT_ID— your Cloudflare account ID, visible in the dashboard URL or under any zone's overview page
These are referenced by name in .github/workflows/deploy.yml. Forks need to add their own values to deploy their own copy.
If this repo was previously connected to Cloudflare Workers Builds via the dashboard's Git integration, disconnect it now. Otherwise every push to main will trigger two parallel deploys: one from GitHub Actions (the new pipeline) and one from Cloudflare's dashboard build (the old one). They will race, the migration gate will be bypassed half the time, and the deploy history becomes a mess.
In the Cloudflare dashboard, navigate to Workers & Pages → emdashcms-org → Settings → Build → Git repository → Disconnect.
After disconnecting, the only path that deploys the worker is the GitHub Actions workflow.
After a workflow run completes, verify production with:
curl -sS -o /dev/null -w "%{http_code} %{size_download}b %{time_starttransfer}s\n" https://emdashcms.org/A 200 with a non-trivial byte count and a sub-300ms TTFB is the happy path. If you see a 5xx, check the workflow run logs and the Cloudflare dashboard worker logs.
D1 has no first-class rollback — migrations are forward-only. If you need to revert the worker code (without touching the database), find the previous successful deploy in the GitHub Actions history and re-run it via Re-run all jobs, or manually:
git revert <bad-commit>
git push origin mainThe revert push triggers a fresh deploy with the previous code. If the bad commit included a migration that needs to be undone, you'll need a new migration that reverses it — there is no automatic rollback for schema changes.