Skip to content

ci: purge the Cloudflare cache after each site deploy - #1189

Merged
vivek7405 merged 2 commits into
mainfrom
ci/purge-cloudflare-cache
Jul 30, 2026
Merged

ci: purge the Cloudflare cache after each site deploy#1189
vivek7405 merged 2 commits into
mainfrom
ci/purge-cloudflare-cache

Conversation

@vivek7405

Copy link
Copy Markdown
Collaborator

Summary

Adds .github/workflows/purge-cdn.yml, which evicts the Cloudflare edge cache after each site deploy, plus a workflow_dispatch trigger for on-demand purges.

webjs.dev serves its static assets with cache-control: public, max-age=14400 at stable urls, so the edge keeps the previous copy for up to four hours after a deploy. That caused two visible regressions in one day (stale tailwind.css after #1179, stale logo marks after #1185), each cleared by a manual dashboard purge.

The workflow waits before purging, and that is the point. Railway auto-deploys from the same push and the build takes minutes. Purging immediately would evict the cache while the origin still serves the OLD bytes, the next visitor repopulates the edge with those bytes, and the site is stale for another four hours with nothing to show for the run. So the job polls GET /__webjs/version (#239) on the Railway origin (bypassing the cache it is about to purge) until the reported uptime is lower than the elapsed time since the run began, which proves the restart belongs to this push. On timeout it fails loudly rather than purging.

Purge is zone-wide: all four hostnames (webjs.dev, example-blog, docs, ui) are proxied inside the one webjs.dev zone, so one call covers them all and cannot silently miss an asset the way a path list does.

Action required before this is useful

Add a repo secret CLOUDFLARE_API_TOKEN, scoped to Zone / Cache Purge / Purge on the webjs.dev zone only (not an account-wide token). Without it the purge step fails with an explicit message rather than passing silently.

Test plan

  • YAML parses; every run block passes bash -n
  • Detection logic exercised against the live origin: with real uptime=459s, elapsed 5s and 100s correctly evaluate "not yet deployed", a large elapsed evaluates "deployed"
  • Verify-step ETag extraction exercised against live origin and edge, correctly reports a match
  • Zone id confirmed against the live API (zone read returns webjs.dev)
  • Repo em-dash check (invariant 11) clean
  • End-to-end on a real merge, which can only be verified once the secret exists and this lands on main

Note on the permanent fix

tailwind.css and the brand marks are referenced by hand-written urls in app/layout.ts and lib/design/brand.ts, so the framework's content-hash ?v= fingerprinting (#243) never touches them. Fingerprinting those urls would remove the need for a purge entirely. That is a separate, larger change; this workflow is worth having regardless as the safety net for anything on a stable url. Documented in framework-dev.md.

Closes #1188

webjs.dev's static assets are served with max-age=14400 at stable urls,
so after a deploy the edge keeps serving the previous copy for up to four
hours. That shipped two visible regressions in one day (a pre-redesign
tailwind.css after #1179, then the un-fixed logo marks after #1185), each
cleared by a manual dashboard purge somebody had to remember. Staleness is
also per-asset rather than all-or-nothing, so the site can sit half-updated
with no signal.

The workflow deliberately does not purge on push. Railway auto-deploys from
the same push and the build takes minutes, so an immediate purge would evict
the cache while the origin still served the old bytes and the next visitor
would repopulate the edge with exactly those bytes, leaving the site stale
for another four hours and burning the run for nothing. Instead it polls
/__webjs/version (#239) on the Railway origin, bypassing the cache it is
about to purge, until the reported uptime is lower than the elapsed time
since the run began, which proves the restart belongs to THIS push rather
than some earlier deploy. On timeout it fails loudly instead of purging.

Purge is zone-wide: all four hostnames are proxied in the one webjs.dev
zone, so a single call covers them and cannot silently miss an asset the
way a hand-maintained path list does. curl uses --fail-with-body and the
response is asserted on .success, since a bare curl exits 0 on a 4xx and
would report a rejected purge as a success.

workflow_dispatch is wired for on-demand purges, which is what the two
incidents above actually needed.

Closes #1188
Self-review of the new workflow turned up four real defects, all in the
wait step's failure handling.

1. Under `set -e`, `uptime=$(... | jq ...)` aborts the whole step when jq
   fails, so a single non-JSON body from the origin (exactly what a
   mid-deploy error page returns) turned a transient blip into a hard
   failure instead of another retry. Every extraction now ends in
   `|| true`, and the numeric comparison defaults to false.

2. The loop was 60 iterations of (10s curl + 15s sleep), a 25 minute
   worst case against `timeout-minutes: 25`, so the job-level kill could
   fire before the step printed why it gave up. The loop is now
   deadline-driven on an explicit 15 minute WAIT_BUDGET, which holds
   whatever the per-request latency turns out to be, leaving 10 minutes
   of headroom for the diagnostic.

3. The verify step reported "Edge matches the origin" when EITHER side
   sent no ETag, and Cloudflare strips ETags under some zone settings, so
   the reassuring message was reachable with no comparison behind it. A
   missing header is now reported as inconclusive.

4. The job declared no `permissions`, inheriting the default token scopes
   despite never checking out the repo or calling the GitHub API. Now
   `permissions: {}`.

Verified by running the real wait step against stub origins: the happy
path (uptime below elapsed) exits 0 after detecting the restart, and a
dead origin exits 1 with the refuse-to-purge guidance.
@vivek7405
vivek7405 marked this pull request as ready for review July 30, 2026 09:44
@vivek7405
vivek7405 merged commit 93883bb into main Jul 30, 2026
19 of 20 checks passed
@vivek7405
vivek7405 deleted the ci/purge-cloudflare-cache branch July 30, 2026 09:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ci: purge the Cloudflare cache automatically after each site deploy

1 participant