CI and deploys
Cloudflare builds and deploys the app; GitHub Actions only runs the tests. Why they are separate, and the minute budget that forced it.
GitHub Actions is not the deploy path and never was. Deploys run entirely on Cloudflare Workers Builds, which is dashboard-managed and has its own quota. A push to staging rebuilds adstack-staging; a push to main rebuilds adstack. If every workflow in .github/workflows/ were deleted, the app would build and ship exactly as it does now. This was verified on 2026-07-29 against the Cloudflare deployments API while Actions was completely out of quota: all five most recent staging pushes deployed normally.
What each layer catches
| Check | Git hooks (local) | CF Workers Build | GitHub Actions |
|---|---|---|---|
| Lint (ultracite / biome) | yes, pre-commit | no | yes |
Typecheck, root (tsc) |
yes, pre-push | no | yes |
| Tests (vitest) | no | no | yes, only here |
Typecheck, cloudflare-agents/ + workers/ |
no | no | yes, only here |
| Bundle assembles | no | yes | no, deliberately removed |
So the unique value of Actions is the test suite and the worker-package typechecks. Everything else there is a backup for hooks that can be bypassed.
The July 2026 overrun
The free tier is 2,000 Actions minutes per month on a private repo, and GitHub bills every job rounded up to a full minute. The old workflow fanned out into 7 parallel jobs averaging 12 to 59 seconds each, so roughly 3.7 minutes of real work billed about 7, and bun install was paid for seven times per run. At 239 runs in July that came to roughly 1,673 billable minutes.
The account hit the ceiling on 2026-07-28. After that every run failed in about 3 seconds with zero steps executed.
Fixes shipped on 2026-07-29:
- 7 jobs collapsed into 1 sequential job sharing one
bun install, with a cache on~/.bun/install/cache - the
buildjob removed, since Cloudflare already builds every push and gates the deploy knipmoved to a weekly Maintenance workflow; it wascontinue-on-errorand gated nothing- the
pull_requesttrigger dropped, since PRs merge tostagingand get checked there paths-ignorefor**/*.md,.claude/**,project/**,docs/**,.vscode/**. 79 of 279 July commits touched only notes and config.
Projected at roughly 400 minutes per month. That is a projection, not a measurement, because the quota was still exhausted when the change shipped. Verify against real billing data before repeating the number.
Current state
Check is disabled manually, to stop failure emails through the end of the July cycle. Re-enable it on or after 2026-08-01:
gh workflow enable "Check"
Then confirm a run actually executes steps rather than failing at zero. Until then there is no automatic test safety net, though deploys are unaffected.
The other workflows
Both cost zero minutes when idle, so neither contributed to the overrun.
| Workflow | Trigger | Status |
|---|---|---|
migrate-production.yml |
push to main |
Dormant because main is dormant. It is the launch-day migration path. Do not delete. |
sync-db-to-staging.yml |
manual only | Destroys staging data and replaces it with a copy of production. |
Watch next
Cloudflare Workers Builds rebuilds on every staging push, including documentation-only ones, and has its own free-tier ceiling. If limit emails start arriving from Cloudflare instead of GitHub, that is the cause, and the fix is a build path filter in the Cloudflare dashboard, not in this repo.