Build and deploy

What sf build and sf deploy actually do: the checks, the per-resource event stream, deployment statuses including PARTIAL, propagation timing, and how to read a failure instead of guessing.

The Spikefrost Team30 Jul 20264 min read

Two commands, and it helps to know what each one is really doing.

sf build

Compiles and validates without shipping anything:

sf build

It runs your app's prebuild (content compilation if the app has any, a full TypeScript check, and platform check scripts) and then the bundle step. It's the fast feedback loop — run it after a change, before a deploy.

The platform checks are the interesting part: they fail the build on patterns that compile fine but break at runtime. React-shaped JSX, Express-shaped request access, unauthenticated operation routes, excessive crypto iterations. See App anatomy for the list.

sf deploy

sf deploy                       # production
sf deploy --env staging         # a named environment
sf deploy -m "add lead export"  # with a commit message

A deploy is commit-anchored: it builds exactly one snapshot of the tree. Under the hood it:

  1. Syncs your local tree if you're deploying from a checkout
  2. Builds and bundles the Worker
  3. Applies any new database migrations, in order, for that environment
  4. Uploads the Worker and attaches its bindings
  5. Reconciles declared configuration — agents, routines, connector claims, queues, search instances
  6. Invalidates the app's edge cache
  7. Flips the version pointer

The command streams per-resource events as they happen. Add --no-follow to fire and return.

Committing and deploying are separate unless you've enabled autodeploy, which ties a branch to an environment — see Environments. Deploying with uncommitted changes works, but it auto-commits under a mechanical message; pass -m so history stays readable.

Reading a deployment

Every attempt — whether you ran it, an agent ran it, or an autodeploy fired — produces a record:

sf deployments                          # newest first
sf deployments logs <deploymentId> --follow

The event stream is per-resource, CloudFormation-style: BUILD, BUNDLE, one event per database migration, WORKER, SECRETS, and each agent/routine/claim reconciliation. When something fails, the last few events name the exact resource and reason. Read them before retrying — most failed deploys fail for a reason a retry won't change.

Statuses

Status Meaning
IN_PROGRESS Running
COMPLETE Everything applied
FAILED Stopped at a named resource; the events say which
SUPERSEDED A newer deploy took over
PARTIAL The worker deployed, but declared-config reconcile failed

PARTIAL is the one to understand. Your site is live and serving the new code — and some agent, routine, or connector change did not apply. It looks like success from the browser. Fix the named configuration file and deploy again; never wave it through.

There is no automatic rollback. Deploys are retry-forward: fix and redeploy, or pin back deliberately with sf deploy --env <env> --commit <id|@N>.

After the deploy

For up to about a minute, some edge locations may still serve the previous HTML while the version pointer propagates. Before concluding a deploy didn't work:

curl -sSI https://your-app.example.com/ | grep -i 'x-sf-'
  • x-sf-version — which deploy answered. Compare with what sf deploy printed.
  • x-sf-cacheHIT means you're looking at a cached copy, not your Worker.

Assets are immutable-keyed, so they're never stale. A cachebusting query string (?v=2) forces a fresh render immediately.

Then verify for real:

sf logs --since 10m --errors     # did anything start throwing?
sf logs --since 10m --expand     # look at specific requests

Request and response bodies aren't captured by default. Opt in per app when you need them:

sf firewall set --body-capture on
sf logs body <rayId>

JSON and text only, secrets redacted, 14-day retention.

Deploying from a coding agent

Agents building your app run the same commands. The convention they follow: sf build to validate as they work, sf deploy when the task is complete — asking first for production environments and destructive changes. If you'd rather nothing ships without you, say so in the conversation; if you want a change live, "deploy it" is enough.

Local development

There's a deliberate gap here: don't try to run the app locally. No wrangler dev, no local dev server, no local database migrations. Local state is not the deployed environment, and treating it as verification is how people ship broken code confidently. The supported loop is sf build for static checks, then deploy to a non-production environment and test the real thing.

Common failures

Symptom Cause
Build fails on className / hooks / react import Hono JSX rules — see App anatomy
Deploy fails naming a Durable Object class Declared in durable-objects.json but not exported from the entry file
Deploy fails on a migration The SQL is invalid for this environment's current schema; check sf d1 migrate list
Deploy is PARTIAL Declared config didn't reconcile; read the events
Site unchanged after a successful deploy Propagation or the edge cache — check x-sf-version and x-sf-cache
Site unchanged, correct version You deployed a different environment than you're looking at

Next

Frequently asked questions

How long does a deploy take?

Usually well under a minute for the deploy itself. Afterwards, for up to about a minute, some edge locations can still serve the previous HTML while the version pointer propagates.

Is there an automatic rollback if a deploy fails?

No. Deploys are retry-forward: read the failing events, fix the cause, and deploy again. To go back to a known-good state deliberately, deploy an older commit with sf deploy --env <env> --commit <id>.

What does a PARTIAL deployment mean?

The worker deployed and your site is live, but reconciling declared configuration — an agent, a routine, a connector claim — failed. Some of your intended change did not apply. Read the events, fix the named file, deploy again. Never ignore it.

Do I have to commit before deploying?

No, but you should. Deploying with uncommitted changes auto-commits under a mechanical message, which makes history harder to read later. Use sf deploy -m "what changed" at minimum.

How do I know which version is live?

The x-sf-version response header names the deploy that answered a request. Compare it to the id sf deploy printed.