What Looked 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.

Before and After

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

Decision Log

Adding the missing push trigger

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.

Handling the environment input that doesn't exist on a push

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.

Filtering which file changes actually trigger a purge

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.

Verifying against the Cloudflare side, not the GitHub side

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.

Frequently asked questions

How does a broken automation run successfully every time?

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.

Why trust Cloudflare's audit log over GitHub's run history?

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.

Why bother filtering which commits trigger a purge?

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.

Does this apply to automation beyond this one workflow?

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.

Passing Isn't the Same as Automatic

A green checkmark proves a workflow executed. It doesn't prove nobody had to click the button to make that happen.