Upgrade the kit

You need: a project. Start with a dry run.

Two steps, and they are different things

pithy upgrade is the command, versions and upgrades is the policy, and compatibility is what the runtime under you promises.

Updating the packages is your package manager’s job.

Reconciling your wiring against what those packages now declare is pithy upgrade’s.

pithy upgrade --dry-run

A dry run resolves no project name and proposes none, so nothing can be written.

What it reconciles

A capability’s manifest grows: a new release declares a binding it did not need before, or a config option it now takes. Upgrade closes that gap.

Five things per Worker:

Missing bindings, per environment — checked for every stanza, since one can be behind while another is current.

Missing config keys, inserted with the manifest’s default as the value and the option’s rationale as the comment above it. An existing key is never rewritten.

Ejected capabilities, named and never touched.

Missing Durable Object exports — the half a config read alone cannot see: the binding is there and the class is nowhere.

Pending migrations, counted. Applied only with --migrate.

Installed is not composed

The manifests are installed once at the project root and shared by every Worker, so they describe every capability installed anywhere in the project — not what this Worker is made of.

A plan is scoped to the Worker’s own composed set. Anything else would put a foreign capability’s bindings, and its class migrations, on a script that never declared them.

What a minor release may change

It may add a required binding, a config option with a default, a route, a table, an error code, and English.

It may not remove a route you call, rename an error code, or change what an existing config key means.

A field added to an admin response is optional rather than merely nullable, because those schemas are the one thing in the kit read across a version boundary — a management client validating a response against a Worker running whatever version it happens to be on.

For a project talking to its own Worker this never comes up. For a dashboard, an additive required key fails validation on every Worker below that release and the whole pane refuses — on the day the dashboard deploys rather than on any day the customer acted.

The compatibility date is not part of an upgrade

It is a behavior contract, not a version number — the date the runtime pretends it is. Every change whose default-on date is later than yours is withheld on purpose, so nothing changes under a deploy nobody made.

Moving it is adopting a set of behavior changes, all at once, and the whole cost is that the set is invisible until something in it matters.

The kit learned this expensively: nine deployed Workers sat fifteen months behind because each config was written by copying a sibling’s — and a day went into a phantom bug only a test harness pinned older than the Worker could produce.

A harness pinned older than the Worker it is evidence about is not evidence.

Move it deliberately, run everything, and name any difference rather than leaving it to be discovered.

Applying

pithy upgrade
pithy upgrade --migrate --env staging

It writes config, one line of source — a Durable Object export — and, with the flag, the database.

It touches no Cloudflare account.

Reading a run that did not fully succeed

Every Worker entry leads with a state, and only one carries a plan.

refused — a decline that cannot be honored. Nothing was written, so there is no half-reconciled wiring to inspect, only a declaration to fix. Refused on a dry run too, because a dry run’s job is to predict the write.

unplanned — the config would not read. Nothing was established and nothing written, so the fields a reconciled entry has would each be a claim nobody checked.

unapplied — the apply failed partway. This Worker’s files have been opened for writing. The plan is what the run set out to do, not what landed — which is exactly what an interrupted apply cannot say.

Every other Worker still reports in full. A run that loses four Workers’ reports to a fifth one’s broken config is the report you cannot use on the day you need it.

Declining a binding you do not want

declinedBindings: {
  SUPPORT_BUCKET: "no R2 in this account yet; the inbox runs without stored bytes",
},

Then upgrade leaves it out and doctor reports it as declined rather than missing — so a stanza you delete by hand stays deleted.

The reason is required, and it is the point. A binding simply absent is indistinguishable from one somebody forgot. Doctor prints your sentence back on every run.

Three declines are refused: a required binding (optional is the capability’s statement that its code has a path for the absence), a Workflow (optional there means not provisioned yet, so declining hides the instruction), and a Durable Object (its class tag is written once and never revisited, so a decline cannot undo what an upgrade stamped).

After upgrading

pithy doctor
pithy migrate --env staging
pithy deploy --env staging

Staging first. That is what it is for.

ESC