← Field Notes
Published 13 min read

The Build Passed. Production Did Not Move.

What a green GitHub build and a failed Cloudflare deployment taught me about separating source validation, deployment credentials, and the live site.

In this article
  1. One change crossed several independent systems
  2. The GitHub build was not lying
  3. Deployment credentials are part of the application path
  4. ”Old production” is a symptom, not a diagnosis
  5. The browser gave me the first useful contradiction
  6. The error message was better than another code change
  7. Retrying a build is not the same as replaying history
  8. A production branch is not a production state
  9. My six-check release contract
  10. The failure was also a credential-lifecycle reminder
  11. The final check still belongs outside the dashboards
  12. What I would do differently next time
  13. Current references
  14. Security and privacy note
  15. AI transparency

A green build can prove that the source compiles. It cannot prove that production changed.

I had just finished one of the largest maintenance passes I have done on Field Notes.

The work touched content, redirects, topic structure, metadata, internal links, privacy wording, and the publication model itself. I moved the changes through a pull request instead of editing production directly. GitHub Actions built the Astro site successfully. The pull request was merged into the production branch.

Then I refreshed the website.

Nothing had changed.

The old article count was still there. An article that had been consolidated was still visible as a standalone post. The new “start here” section was missing. The site looked exactly like it had before the merge.

My first instinct could have been to question the code again.

That would have been the wrong layer.

The source had changed. GitHub had accepted the merge. The repository build had passed. The missing step was somewhere between validated source and active production deployment.

Cloudflare’s build log made the boundary explicit:

The build token selected for this build has been deleted or rolled

The remainder of the log instructed me to select a valid build token and retry.

The website was not serving stale code because Astro had secretly ignored the merge.

Production had not moved because the system responsible for deploying the Worker no longer had a valid credential to perform the deployment.

That distinction sounds obvious once the log is in front of me.

It was much less obvious from the browser.

One change crossed several independent systems

For this site, a normal production change crosses several boundaries:

local/editorial work
        |
        v
Git repository
        |
        v
pull request
        |
        v
GitHub Actions build
        |
        v
merge to main
        |
        v
Cloudflare Workers Build
        |
        v
Worker deployment
        |
        v
public hostname
        |
        v
browser-visible result

Those steps are related.

They are not interchangeable.

My repository’s GitHub Actions workflow checks out the source, installs dependencies, and runs the Astro build. A passing workflow tells me the source can produce the site’s build output in that CI environment.

Cloudflare Workers Builds has a different responsibility. It receives the production-branch change, runs its configured build process, and deploys the Worker using credentials that authorize Cloudflare to upload and activate the result.

The public site is another boundary again.

Even after a deployment reports success, I still need to prove that the expected hostname, route, redirect, metadata, content, and user-facing behavior are actually present.

The mistake would be treating any one of those green states as proof for all of the others.

The GitHub build was not lying

It is tempting to call a successful CI check misleading when production still shows the old site.

It was not.

The GitHub workflow answered the question it had been configured to answer:

Can this repository install its dependencies and build the Astro site successfully?

For this repository, the workflow runs:

- name: Install dependencies
  run: npm ci

- name: Build Astro site
  run: npm run build

That check was useful.

It caught syntax, import, content, route, and build-time problems before the change reached production. It gave me confidence that the large remediation branch was not obviously broken.

It simply did not own the Cloudflare deployment credential.

That belongs to another system.

This is the same reason I try to give tests narrow names. A result becomes dangerous when I let it answer a larger question than the test actually asked.

“Build passed” was accurate.

“Production updated” had not been tested yet.

Deployment credentials are part of the application path

I normally think about credentials as things an application uses at runtime.

Database passwords. API keys. service tokens. Authentication secrets.

CI/CD credentials belong in the same operational model even though users never see them.

Cloudflare Workers Builds uses an API token to authorize build and deployment activity against the Cloudflare account. Cloudflare’s current documentation says that Workers Builds can automatically create a token, or use one selected by the account owner. That token authorizes the build system to interact with the resources needed to deploy the Worker.

If that token is deleted, rotated, or otherwise becomes stale, the source repository can remain completely healthy while automated production deployment stops.

That is exactly what happened here.

Nothing in the Astro source needed to change to repair the deployment path.

The failing component was authorization between the build system and the platform it needed to modify.

That changed how I describe deployment health.

A working pipeline requires more than:

  • valid source;
  • valid dependencies;
  • a successful build command.

It also requires:

  • a valid production trigger;
  • a valid deployment command;
  • valid deployment credentials;
  • access to the intended account and Worker;
  • a successful activation step;
  • and a final public validation.

Credentials are not an administrative detail outside the architecture.

They are one of the dependencies.

”Old production” is a symptom, not a diagnosis

When a site does not change after a merge, several explanations are plausible.

The browser could be showing cached content.

The wrong branch could be configured as production.

The platform might not have received the repository event.

The build could have failed.

The build could have succeeded while deployment failed.

The deployment could have targeted the wrong Worker.

The Worker could be correct while the custom domain still points elsewhere.

The change could actually be live, but the page I am checking might not exercise it.

Those are very different failures.

So I now treat “production is old” as the start of a small decision tree.

EvidenceWhat it provesWhat it does not prove
Commit exists on mainThe source change reached the production branchThat any deployment system processed it
GitHub Actions is greenThe repository completed its configured CI buildThat Cloudflare authenticated or deployed
Cloudflare build startedThe Git integration saw a triggerThat the build or deploy completed
Cloudflare build completedThe configured Cloudflare build/deploy process finishedThat the intended custom domain serves the new result
Expected public marker appearsThe checked route serves the expected versionThat every route, redirect, or metadata change is correct
Final verification matrix passesThe selected production requirements were demonstratedThat untested behavior is also correct

That table keeps me from skipping layers because one dashboard is green.

The browser gave me the first useful contradiction

The most useful test after the merge was not sophisticated.

I knew the remediation changed several visible things.

The new site should have had:

  • a lower published article count because overlapping posts were consolidated;
  • a new “Solve a problem / Start here” section;
  • retired article URLs redirecting to retained articles;
  • different topic behavior;
  • updated privacy text.

I refreshed the homepage and did not see those markers.

That contradiction was enough.

I did not need to immediately inspect every route or clear every cache.

The source history said the change existed.

The public page said the change was not active.

That narrowed the problem to the path between source and serving.

From there, the deployment log was the right evidence source.

This is one reason I like choosing a few obvious production markers before a deployment. A release becomes easier to verify when I know exactly what should be different afterward.

The error message was better than another code change

The Cloudflare log was unusually direct.

It said the selected build token had been deleted or rolled and could not be used. Cloudflare’s current troubleshooting documentation describes that exact stale-token condition and says to create a new API token, select it in the Worker’s build configuration, and retry the build.

The important part for me was not the fix itself.

It was the discipline of accepting the evidence.

The code did not need another commit.

The repository did not need a fake whitespace change just to kick the pipeline.

Astro did not need to be rebuilt locally for the tenth time.

The problem was already named.

That sounds obvious, but repeated deployment attempts can create a bad habit: when production is wrong, touch the source until something happens.

That makes the incident harder to understand because source changes become mixed with platform recovery.

If the failure is a stale deployment token, repair the deployment token.

Keep the application change and the deployment repair as separate concepts.

Retrying a build is not the same as replaying history

Cloudflare’s current Workers Builds documentation notes that when build settings are updated, the next build uses the new settings. It also says that when an existing build is retried, the build configuration that exists at retry time is applied.

That matters operationally.

I do not need to manufacture a new application commit just because the original deployment attempt used stale platform configuration.

Once the build token is corrected, retrying the failed build can use the repaired configuration against the intended source revision.

That preserves a cleaner story:

  1. application change merged;
  2. deployment failed because its credential was stale;
  3. platform credential repaired;
  4. same intended release retried;
  5. production result verified.

That sequence is much easier to explain later than “I pushed three random commits until the website changed.”

A production branch is not a production state

This incident also corrected a phrase I use casually.

I often call main the production branch.

That is reasonable shorthand.

But the branch itself is not production.

It is the source branch that is configured to trigger the production deployment path.

Cloudflare’s current Workers Builds documentation describes production builds in those terms: commits to the configured production branch trigger the build and deploy commands.

The live Worker remains its own state.

That means several versions can matter at once:

  • the source currently in main;
  • the source revision used by the latest build;
  • the Worker version produced by that build;
  • the active deployment;
  • the behavior currently visible through the public hostname.

Most of the time those move together quickly enough that the distinction feels academic.

During a failure, the distinction is the whole problem.

My six-check release contract

The site already had build checks.

What I needed was a clearer release contract.

For a normal Field Notes change, I now want six separate pieces of evidence: the exact source commit, repository CI run, platform trigger, platform build/deploy result, active Worker version, and a preselected public marker. The public marker must be chosen before merging, and should not reveal private infrastructure. This is the same bounded-testing principle behind The Test Matrix I Use Before I Trust a Change.

My practical release record looks like this:

Source:
  expected commit is on main

CI:
  repository build completes successfully

Platform:
  Cloudflare build is triggered from main
  build token is valid
  build command succeeds
  deploy command succeeds

Edge:
  canonical hostname serves the deployment
  www redirect preserves path and query
  ads.txt remains reachable where required

Application:
  homepage shows one expected release marker
  changed routes behave as intended
  retired URLs redirect correctly
  metadata/canonical output matches the intended domain

Editorial:
  intended article count is correct
  no draft or retired content is accidentally published

Not every deployment needs a ceremony.

It does need an end state.

The release contract tells me where to look when that end state is missing.

The failure was also a credential-lifecycle reminder

Tokens do not remain trustworthy merely because the application source is unchanged.

A credential can be:

  • deleted;
  • rotated;
  • replaced;
  • scoped differently;
  • disconnected from the integration that originally used it;
  • or left stale in a platform configuration after account maintenance.

That means CI/CD configuration has maintenance requirements of its own.

I do not want to solve that by creating permanent, excessively broad credentials just to avoid future failures.

The better response is to know:

  • which system owns the token;
  • what the token is allowed to do;
  • where the token is selected;
  • how a replacement is created;
  • what breaks when it expires or is revoked;
  • and how to validate a new token without exposing it.

Cloudflare’s current Workers Builds configuration documentation explicitly treats the API token as the credential that authenticates the build request and authorizes upload and deployment.

That is exactly the role I want documented.

The secret value should remain secret.

Its operational purpose should not.

The final check still belongs outside the dashboards

Once the deployment path is repaired, I still do not want to finish with “Cloudflare says success.”

The final check comes back to the public site.

For this release, I already know several things that should be observable without privileged access.

I can verify that the homepage has the new navigation and article-discovery structure.

I can verify that the published article count reflects the consolidation.

I can request retired URLs and confirm that they redirect directly to their retained destinations.

I can inspect /ads.txt.

I can test the www hostname and prove that the redirect keeps both the requested path and query string.

I can inspect pages that should carry noindex,follow and confirm they are not being promoted in the sitemap.

Those tests cross the same boundary a real visitor or crawler crosses.

That is why they matter. How I Kept Drafts Out of Astro Production Builds covers a different boundary: excluding unfinished content from output. This incident was about the intended output failing to reach production.

A deployment system can tell me what it attempted.

Cloudflare’s current Workers Builds documentation makes one more distinction useful here: a successful production-branch build can create a Worker version, while the active deployment is the state that actually serves production. Cloudflare also documents that a production push runs the configured build command followed by the deploy command. That gives me a sharper validation order: identify the source revision, confirm the build processed it, confirm the deploy step promoted the intended result, then verify the public hostname.

The public request tells me what I actually shipped.

What I would do differently next time

For a meaningful site change, I would choose the post-deployment evidence before merging.

I would write down:

  • one source marker;
  • one CI marker;
  • one platform deployment marker;
  • one public content marker;
  • any changed redirect or route that must be exercised;
  • and the recovery step if production does not move.

I would also treat the Worker build credential as a documented deployment dependency instead of something that exists only in the Cloudflare dashboard.

Most importantly, I would resist touching the application when the evidence points somewhere else.

The repository had already passed its test.

The live site had already failed its test.

The deployment log told me why.

Once each layer was allowed to answer only its own question, the incident became much smaller.

Current references

Cloudflare-specific behavior in this draft was rechecked on October 8, 2026 against current Cloudflare documentation. Current Workers documentation confirms that pushes to the configured production branch run the build command followed by the deploy command, and that a successful production build can create a new Worker version without making it the Active Deployment when automatic deployment is disabled. That distinction is central to the release model above.

Security and privacy note

This article describes the deployment failure without exposing the API token value, account identifiers, private environment variables, repository credentials, or other secrets. Public project names, general platform behavior, and the sanitized error message are sufficient to explain the failure.

AI transparency

AI assisted with structure, editing, overlap review, and verification against current Cloudflare documentation. The remediation merge, successful repository build, unchanged production site, stale Cloudflare build-token error, and troubleshooting sequence are based on the actual Eldritch IT Field Notes deployment on October 1, 2026.

JO

Written by

Jessie Owens

I run Eldritch IT and write about the systems, repairs, infrastructure decisions, and business lessons behind the work.