Environment per feature

You need: a git repository and a project name. The live half also needs a Cloudflare account.

Two halves, and they are separate on purpose

pithy feature drives the local half, pithy provision --feature the cloud half, and worktree development is what the local half actually is.

The local half is a worktree, a port block and a Miniflare-backed backend. No account, no network, nothing to clean up remotely.

The live half is real D1, KV and R2 named from the branch, plus deployed Workers.

You can have the first without the second, which is what most work actually needs.

Creating one

pithy feature create media-cli --issue 69

Validates the issue number and the slug at the boundary — before a branch or a worktree exists, so a refusal leaves nothing behind. Then it cuts the worktree, installs, reserves a port block, pins one port per Worker, and migrates and seeds the local backend.

It cuts from local main, not from the remote

gitGraph
   commit id: "shared" tag: "origin/main"
   commit id: "unpushed work"
   branch feature/69-media-cli
   commit id: "the feature"

A repository holding unpushed work would otherwise start every feature before that work — and where the older tree still loads there is no symptom at all, just a branch rooted in the past, found at merge.

When local main is behind its remote the run says so and carries on, because cutting from a slightly stale main is usually fine and sometimes deliberate. Being told is what stops it becoming a surprise.

Provisioning the live half

pithy provision --feature

From inside the worktree. The branch says which feature it is.

Nothing is stored to make this work. Every name is derived from the branch, and an already-provisioned resource is recovered by looking its name up in Cloudflare — which is what makes it idempotent and resumable. On a second push CI computes the same names, finds the existing resources, rewrites the same wiring, and deploys.

There is no id file to merge, so there is nothing to conflict.

The ids go to a build artifact, not to source

Into a git-ignored path, generated from the tracked config on every run.

So the tracked file is never dirty, git add -A cannot reach the ids, and a feature abandoned without teardown strands nothing.

Every run states the file it wrote and what happens to it, because a single flag that flips whether output is committed will eventually surprise somebody.

One consequence worth knowing: pithy env reports what a Worker’s tracked config declares, so a feature environment does not appear there.

The naming, and the budget

<project>-f<issue>-<slug>-<binding>-<kind>

A feature environment is an environment, so it occupies the environment slot of the one rule every other name follows.

There is no Worker segment. Two Workers that both declare the same binding are backed by one resource — sharing is expressed in the binding name.

This is the tightest shape the kit composes, and it is what caps the project name. Keep the part of a branch name after the issue number to roughly twenty characters: a slug over budget is truncated to a head plus a hash rather than refused — failing CI over a long branch name would be the worse failure — but a hashed slug tells nobody reading a bucket listing which branch owns it.

It gets its own master key, and no secrets manager

The key is the feature’s own, and teardown deletes it — the opposite of the rule for a declared environment, where losing a key orphans every secret. For something ephemeral that reasoning inverts.

A feature gets no secrets manager, deliberately. A manager is a Worker with its own database and its own rotation cron, and one per open pull request is not a cost a branch should carry.

So a secret a feature needs beyond its master key is one the report names as unbound, with the command that creates it. That is a stated shortfall rather than an invented remedy, and the run warns rather than refusing — because this runs per pull request, and failing every one of them would not close the gap.

Picking up somebody else’s branch

pithy feature sync

None of the local state is in git. So sync creates it on your machine, with your own free block — which is precisely why ports are never committed: your teammate’s block may already be taken here.

It also covers the other everyday case: you added a Worker, and it takes the next free port from the block you already hold.

Tearing down

pithy feature destroy

Deletes the resources by the exact ids in the worktree’s manifest first, then by recomputing every name the enabled capabilities could have produced and deleting what still exists — which is what catches a partly-failed provision.

Then it frees the port block, prunes the worktree, and deletes the branch if it has been merged.

It does not need a Worker config to load. Its local half is derived from the branch and the root config — deliberately, because the state it is most needed in is a create that failed partway, which leaves a worktree whose config throws. A teardown that loaded it first was unavailable in exactly that state, and the port block leaked to a branch that no longer existed.

Missing credentials are a hard failure rather than a silent skip. Skipping would leak every resource while reporting success — and the teardown then deletes the branch the names are derived from, so a later attempt could no longer work out what to delete.

--local-only is the deliberate opt-out, for a laptop with no credentials.

In a pipeline

- run: pithy provision --feature --json
# on merge
- run: pithy feature destroy --json

Neither leaves anything to commit.

Check it worked

  • Two features run locally at once, on different ports
  • A colleague’s branch syncs on your machine with its own block
  • Provisioning twice creates nothing the second time
  • Teardown deletes what provisioning made, and reports what it deleted
  • Nothing in the tracked tree is dirty afterwards
ESC