My deploy said 'complete' and changed nothing
, 5 min read
I shipped a batch of fixes to this site — security headers, self-hosted fonts, a corrected canonical — built it, deployed it, and got this:
✨ Success! Uploaded 43 files (28 already uploaded) (1.14 sec)
✨ Uploading _headers
🌎 Deploying...
✨ Deployment complete! Take a peek over at https://9afb7e5d.nabin-portfolio.pages.dev Then I checked the live site, because several of those changes were response
headers and generated files that only exist once the host has them. The new /llms.txt returned 404. The self-hosted fonts returned 404. The page still
carried the old og:locale. None of it was there.
A deploy that fails is a problem you start solving immediately. A deploy that reports success and changes nothing sends you somewhere else entirely — to caching, to edge propagation, to header precedence. I spent a while building increasingly elaborate theories about Cloudflare’s cache before considering that the deployment might not have gone where I thought.
Every one of those theories was about why the right deployment might not be visible yet. None of them questioned whether it was the right deployment.
The thing I was looking at without seeing
Read the last line again:
✨ Deployment complete! Take a peek over at https://9afb7e5d.nabin-portfolio.pages.dev
✨ Deployment alias URL: https://master.nabin-portfolio.pages.dev master.nabin-portfolio.pages.dev. A branch alias. That is what a preview deployment looks like, and nothing else in the output says so.
No warning, no colour, no “this is not production”. Just an extra line that
reads like a bonus convenience URL.
The deployment was real, the build was correct, every file uploaded. It went somewhere nobody visits.
Why
Two facts that are individually reasonable and jointly a trap.
One. This repository’s branch is master. The Cloudflare Pages project’s
production branch is main. They have never matched — the repo predates the
Pages project, and nothing ever forced the question.
Two. wrangler pages deploy takes a --branch flag. Its help text
documents what the flag does but not what happens when you omit it:
--branch The name of the branch you want to deploy to [string] I assumed it inferred the branch from git, but assuming was how I got here,
so I tested it. I ran a deploy with no --branch flag at all and asked
Cloudflare what it recorded:
$ git rev-parse --abbrev-ref HEAD
master
$ npx wrangler pages deploy build --project-name=nabin-portfolio
✨ Deployment complete! ...
✨ Deployment alias URL: https://master.nabin-portfolio.pages.dev
$ npx wrangler pages deployment list --project-name=nabin-portfolio
id=ad785a61 env=Preview branch=master commit=8a084d2 Confirmed. Wrangler reads the current git branch and deploys to a Pages
branch of that name. Since master is not this project’s production branch,
every deployment made the obvious way is a preview — correct, complete,
uploaded, and invisible.
The record makes the shape of it plain. The same commit appears twice, five minutes apart:
9afb7e5d Preview master eeea7e9
3ba45e5c Production main eeea7e9 Identical content. One reached the site. The difference is a string.
I also found an earlier preview from the same commit, from before I started paying attention. This had already happened once and gone unnoticed, which is exactly what you would expect from a failure mode whose only symptom is green text.
The other half: pushing deploys nothing
While working this out I checked how the project is wired, and found a second assumption worth killing.
$ npx wrangler pages project list
│ nabin-portfolio │ nabin-portfolio.pages.dev, nabenshrestha.com.np │ No │ That No is the Git Provider column. The project is a direct upload project: it has no connection to GitHub at all. Pushing to master deploys
nothing, ever. The build happens locally and Cloudflare only receives the
finished directory.
I had documented the opposite. The repo’s DEPLOY.md described a
Git-connected project with a build command, a production branch, and “every
push to master redeploys”. Every word of that was wrong, and it had been
wrong long enough that I trusted it instead of checking.
So there were two silent failures stacked on each other: a push that deploys nothing, and a deploy that goes to a preview. Between them, the reasonable belief “I pushed and then I deployed, so it is live” was false twice over.
Direct-upload projects are not a mistake, incidentally — they are the right choice when you want the build to happen somewhere you control, with your own Node version and your own lockfile discipline, rather than in a hosted builder you configure through a web form. The mistake was having a document that described the other kind, and never rereading it once it stopped being true. Deployment documentation rots faster than code because nothing executes it.
The fix, and the check
The fix is a flag:
npx wrangler pages deploy build --project-name=nabin-portfolio --branch=main The check is the part that matters, because the flag is exactly the kind of thing that gets dropped from a command someone retypes from memory:
npx wrangler pages deployment list --project-name=nabin-portfolio | head -6 The newest row must read Production. That column is the only thing in this
entire system that distinguishes a deploy that worked from a deploy that
reported working.
The deeper fix is to stop the two names disagreeing — rename the git branch
to main, or change the Pages production branch to master. I have not
done it yet, and I am not sure the mismatch is the real bug. The real bug is
a tool that infers a critical destination from ambient state and reports
success identically either way.
Inference from ambient state is fine when being wrong is cheap. ls guesses
the current directory; if that is not what you meant, you see it instantly
and type another command. Here the ambient state is a git branch name, the
destination is production, and being wrong looks exactly like being right.
The wider the gap between “how much of this was guessed” and “how bad is it
if the guess was wrong”, the louder the tool should be — and this one is
silent.
I would settle for one extra line in the output: deployed to preview (branch: master); production branch is main. Everything needed to print it was known at the time.
What I actually changed about how I work
Every deploy of this site now ends with two things: the deployment list check above, and a verification of something that can only be true if the
new build is live. For me that is a header, because headers are host-applied
and cannot be faked by a stale local build:
curl -sI https://nabenshrestha.com.np/ | grep -i strict-transport Empty means it did not land, whatever the deploy said.
The general lesson is not about Cloudflare. It is that “the command succeeded” and “the change is live” are different claims, and tools routinely let you confuse them by reporting the first in the language of the second. Anything that ends in a green tick deserves one question: what, specifically, would be different in the world if this had worked? Then go and look at that thing instead.
For a deploy, the answer is never the deploy log. It is the site.