Release Workflow

How a change reaches production. Four steps, with the same names and commands in every repository. All of it is a default. Skip or change anything when the app has a reason, and write the reason in that repository's release.md.

The New App Workflow gets an app to its first release. This is every change after that.

Local Checks Preview Production a laptop, against stand-ins lint · tests · types, automatic private site, built from a branch live, on a written go

Main is always releasable.

Preview and production both build from main. An app that can't keep this says why in release.md and names what replaces it, usually a release branch.

The four steps

#StepWhat it isHow work gets inOutside providers (Stripe, Resend)
01 Local Runs on a laptop against stand-ins. No live data, no live accounts. Anyone, any time. A stand-in, so no key is needed. Test mode with keys in .env.local when the real API matters. Email renders locally and nothing sends.
02 Checks Lint, tests, and types on every pull request. A failing change doesn't move on. Automatic. No network. Tests mock the provider at its seam.
03 Preview A private site built from a branch. Real code, real look, no way to harm live data. The client and Will review here. Automatic on push. No approval, because preview can't hurt anything. Test mode. Stripe test keys and cards. Resend limited to an allow-list or its test addresses. Webhooks point at the preview address with their own secret.
04 Production The live site. One command to release, one to go back, a record of each. A human says go, in writing, after seeing the preview. Live keys, only in the host's production environment. Prices, webhooks, and sending domains are live settings with their own go.

Into main: a pull request with checks green and a review. Merging is not a release. Back out: one command restores the previous deployment and verifies. Live settings, domains, schema, jobs, and provider settings each get their own go.

The defaults

DefaultWhy
Departures are written downIn release.md, so nobody rediscovers them.
The four names and the six commands: dev, check, preview, release, verify, rollback"Staging" means preview. The host can differ. The words a person or agent types don't.
One release.md with the same headingsAddresses, jobs, providers, record format, restore steps, departures.
The gate sentences in AGENTS.mdMerge is not a release. No live change without a go.
Preview can't change live dataA read-only role, or a separate copy. The rule is fixed. The mechanism isn't.
Nothing real fires from previewNo jobs, no customer email, no live-mode writes. Test-mode traffic is fine.
Secrets live in the host, per environmentLocal uses .env.local. The repo holds .env.example with names only.
Agent permissions are committedClaude Code: .claude/settings.json. Codex: .codex/rules/. Nothing depends on one laptop.

What differs: the host (Vercel, Cloudflare), the login provider (Memberful, Clerk), the database (Supabase, Convex), the outside providers, scheduled work, the runtime version. All of it sits behind the names above.

The commands

Thin wrappers, one job each. Policy lives in AGENTS.md and the host's settings, not here.

CommandDoesLeaves alone
pnpm devStarts the app against stand-insLive services
pnpm checkLint, tests, types, runtime pinnedDeploying
pnpm previewPuts the work where the preview builds fromProduction
pnpm releaseBuilds from a clean copy of main, deploys, verifies, prints the recordRunning without the go
pnpm verifyRead-only checks: addresses, deployment id, job owner, provider modeChanging anything
pnpm rollbackReturns production to the previous deployment, then verifiesRebuilding code

In a monorepo

The workflow lives at the root, where a person or agent starts. The apps hold the how.

WhatWhere
The commandsRoot package.json, suffixed per app: pnpm release:web. Each calls the app's own scripts. pnpm check alone runs every app.
release.mdOne file at the root, docs/agents/release.md, with a Facts and Commands block per app.
The mechanicsInside each app: host config, build scripts, .env.example.
The recordNames the app. Releasing one says nothing about its siblings.

One app, no suffix. A block that outgrows the file moves to docs/agents/release/<app>.md, same headings.

The two files

docs/agents/release.md
# <Repo> release

Merging to main is not a release. <Who> reviews the preview and says go.

## Departures
<defaults this repo skips or changes, and why>

## Facts: <app>
| Host project         | <name, id> |
| Production addresses | <list> |
| Preview address      | <address>, protected by <method> |
| Preview data         | <read-only role or separate copy> |
| Scheduled jobs       | <list>; production only |
| Providers            | <name>: test mode in preview, live in production |
| Records              | <ticket tracker> |

## Commands: <app>
| Local    | `pnpm dev:<app>` |
| Checks   | `pnpm check:<app>` |
| Preview  | `pnpm preview:<app>` |
| Release  | `pnpm release:<app>` |
| Verify   | `pnpm verify:<app> --env production` |
| Rollback | `pnpm rollback:<app> <deployment>` |

## Record template
<date, app, commit, deployment id, addresses, checks, restore target>

## Restore
<steps, and what a rollback does not restore>

## Agent tool notes
<limits, workarounds, where each agent's permission file is>
AGENTS.md · the gate sentences
Open pull requests. Do not merge without the user's instruction.

Merging is not a release. Release, roll back, or change live settings, domains, schema, jobs, or provider settings only after the user's explicit go.

Preview can't change live data, runs no jobs, and talks to providers in test mode. Use it for every review.

Local uses stand-ins and .env.local. Never copy production values into the repository or a record.

Departures and tool limits: docs/agents/release.md.