Skip to content

Engineering · 3 min read

Shipping and releasing

How a change actually gets to production here: small pull requests, a trunk that's always releasable, and deploys boring enough to do on a Friday.

This is the mechanical companion to how we build: not the philosophy of shipping small, but what the path from your machine to production actually looks like. The whole thing is designed so that releasing is uneventful. A release that makes anyone nervous is a bug in the process.

The path a change takes

  1. Branch off main. Keep the change small and focused. If it's growing arms and legs, that's a signal to split it, not to push on.
  2. Open a pull request. Describe what it does and, more usefully, why. Draft it early if you want eyes before it's finished.
  3. Get it reviewed. Small changes get fast, real reviews; that's the deal in how we review code, and the step-by-step is in how we review PRs. Keep the noisy commits (a rename, a formatting pass) separate from the substantive one so the reviewer isn't hunting for signal.
  4. Merge to main. Checks run the tests and, when they're green, the change deploys. How it deploys is up to each product and written in its README: this website goes out on Vercel, and g1t lands changes through its own merge queue, which tests each one with what's ahead of it before it reaches main. Either way, there's no separate "release" ceremony to schedule.

Main stays releasable, always

main should be deployable at any moment, not after someone finishes one more fix. This is the single rule that makes everything else work: when the trunk is always green, shipping is routine and a release is a non-event. If main isn't releasable, fixing that is the most important thing in the building until it is.

If you'd be uncomfortable with main deploying to production by accident right now, it isn't ready. Get it back to green before you do anything else.

Ship behind a flag when it's not ready to be seen

A change being merged and a feature being visible are two different events, and it's often right to separate them. Merge the code early, in small pieces, with the new behavior gated off. Then turn it on for yourself, then for a few friendly users, then for everyone, watching as you widen the circle. This is what lets us ship the smallest useful change without waiting for a whole feature to be finished before any of it lands.

It also means a bad release is a config change to reverse, not a frantic rollback.

Continuous for what we run, deliberate for what others depend on

The sites and services we run ourselves deploy continuously, because we own both ends and can fix forward fast. The things other people build against are different. A product's public API is a contract: a breaking change gets versioned and announced, not slipped out, which is the whole point of customer comms as an engineer. Anything people install, an SDK, a CLI, a library, releases the same way: a tagged version, a changelog entry a human can actually read, and semver that means what it says. Breaking someone's build to save yourself a few minutes of care is not a trade we make.

Each product announces its own releases

Shipping is owned by the product, and so is telling people about it. Each product keeps its own public roadmap and changelog on its own site, and announces what it ships there, in its own voice. The company site doesn't keep a combined roadmap or changelog; it introduces the products and points you to them. When a release is big enough to be company news, it can get a post on the blog too, but the product's own site is where the record lives.

When a release goes wrong

It will, sometimes. The response is calm and practiced: flip the flag off or roll forward with a fix, get users back to working, and only then figure out what happened. The full playbook, including the blameless write-up afterward, is in when things break. The goal is never to find who to blame. It's to make the system a little harder to break the same way twice.