The Purge That Only Ran Manually
An automation that never once failed - because it was never actually automated.
The workflow had a name, a schedule of sorts, and a green checkmark
on every run. purge-cache.yml sat in
.github/workflows, and every time it ran, it succeeded
- Cloudflare's cache got purged, the site reflected changes, nothing
looked wrong. That was the problem. Nothing looked wrong because
nothing had ever actually broken. It just wasn't doing what it was
named to do.
The tell wasn't an error. It was checking the Actions run history out of routine and noticing every single entry read the same way: Manually run by scottlabz. Not one of them said triggered by a push, because nothing in the workflow file was listening for one.
The trigger block only declared
workflow_dispatch - the event that fires when someone
clicks "Run workflow" in the Actions tab. There was no
push trigger anywhere in the file. Every purge that had
ever happened, going back through the entire run history, was a
manual click wearing the outfit of a pipeline.
| What | Before | After |
|---|---|---|
| Trigger block |
workflow_dispatch only - runs only when someone
clicks the button
|
workflow_dispatch plus push,
scoped to main
|
| Environment resolution |
Hard reference to inputs.environment, which
only exists on manual dispatch runs
|
inputs.environment || 'production'
fallback, so push-triggered runs still resolve
|
| What triggers a purge | Nothing did, automatically - every commit sat there until someone remembered to click |
Any push to main touching a content-relevant
file
|
| Cloudflare audit log actor |
Actor Context: dash on every entry -
dashboard-style, human-initiated
|
Actor Context: api_token, blank actor email -
confirms the API, not a person, made the call
|
This was the actual fix. A push event scoped to
main was added alongside the existing
workflow_dispatch, so manual runs still work when
needed, but a deploy no longer depends on someone remembering to
trigger it by hand.
The workflow referenced inputs.environment
directly, which is only populated when a run starts from
workflow_dispatch. Left as-is, a push-triggered run
would hit an empty value. Adding
inputs.environment || 'production' gives
push-triggered runs a sane default without changing behavior for
manual runs, which still pass whatever environment was chosen at
dispatch.
Once the push trigger existed, the next question was whether every
commit should fire it. It shouldn't. Editing
CLAUDE.md, a Python script, or the workflow file
itself doesn't change anything a visitor would see, and purging on
those commits anyway just burns purge requests and adds noise to
the audit log. A paths: filter was added covering
.html, .css, .js,
.json, .xml, .svg, and
common image extensions - the file types that actually change
what's served.
A green checkmark in Actions only proves the workflow executed,
not that it executed the way it was supposed to - a manual click
produces the identical checkmark. The real confirmation came from
Cloudflare's own audit log, where each purge request records an
Actor Context: dash for a
dashboard-style action, api_token for one
authenticated by the workflow itself, with a blank actor email as
the second confirming signal. That's the only place the
distinction between "a person did this" and "this ran on its own"
actually shows up.
Because a manual trigger and an automated trigger produce the same kind of run in GitHub Actions - same log, same green checkmark. Nothing about the interface distinguishes "this fired on its own" from "someone just clicked the button," so a workflow can look perfectly healthy while never once running unattended.
GitHub can confirm the workflow executed. It can't tell you what actually triggered that execution from the outside. Cloudflare's audit log records who or what authenticated the actual purge request, which is the only signal that separates a human click from an API-driven call.
Because not every commit changes anything visitors see. Purging on documentation edits or CI config changes wastes purge requests and buries the log in entries that don't mean anything, making it harder to notice a real problem the next time one shows up.
Yes - a passing status only proves something ran, not that it ran the way it was designed to. Anything that's supposed to be automatic needs a way to prove it was automatic, from a signal that can't be produced by a person doing it manually instead.
A green checkmark proves a workflow executed. It doesn't prove nobody had to click the button to make that happen.