Skip to content
AdStack Engineering Docs
Esc
navigateopen⌘Jpreview
On this page

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 build job removed, since Cloudflare already builds every push and gates the deploy
  • knip moved to a weekly Maintenance workflow; it was continue-on-error and gated nothing
  • the pull_request trigger dropped, since PRs merge to staging and get checked there
  • paths-ignore for **/*.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.

Last updated on July 30, 2026

Was this page helpful?