Merged is not live.Copy link
Staging a build and releasing it on purpose is ancient, boring practice, and almost nobody runs it at the size of one person. Agents writing most of my code changed that maths. The release model I put on three live apps in a weekend, and why I left myself no way round it.
Staging a build and releasing it on purpose is not a new idea. Build once, promote the artefact you already looked at, keep a way back to the last one. Release managers have been doing that since before I started and they will be doing it long after I stop. If you have worked anywhere with a change board you can probably guess every mechanism in this piece from the title, and you would be right.
So this isn't me discovering process. For as long as I've run apps of my own, merging a branch into main has meant the change is live. Merge, wait a minute, refresh, done. I knew what the grown-up version looked like. I just didn't run it, on the reasonable grounds that release ceremony was invented to stop a lot of people standing on each other, and there was only one of me.
That's the argument I'd have made a year ago and I don't think it was a stupid one. What killed it is that the only person touching the code isn't me any more. Most of what lands in my repositories now is written by AI agents working to my instructions. The volume of change went up like a team had joined; my attention stayed exactly where it was. Once that's true, the boring version starts paying for itself, and being careful stops being a plan.
So last weekend I went and did the boring thing properly. Three live apps moved: my own, a martial arts club I build for, and that club's members portal, which is the one with paying members sitting behind a login. The new rule is one sentence long. Merging to main doesn't publish anything any more. It stages it. Going live is a separate thing I have to ask for.
What a merge does nowCopy link
The apps run on Cloudflare Workers, which for the purposes of this piece is just "the platform that serves them". Workers has a concept called versions. You upload a new build, you get back a version identifier and a private preview URL for that exact build, and live traffic carries on hitting whatever version it was already hitting. Nothing moves. That upload is now the entire job of merging to main.
So a merge produces a candidate. Tests run, the build happens once, and what comes out the other end is a specific numbered version sitting on a shelf with its own URL. I can open that URL, click round the real thing, and production is exactly where I left it.
Going live is a second workflow that only runs when a human asks for it. In GitHub Actions terms it's a workflow_dispatch job, which is a formal way of saying it has no trigger at all except somebody choosing to run it. Actions, Release, Run. Leave the input blank and it promotes the newest staged version. Type a version identifier in and it promotes that one instead.
Rollback is the same button pointed backwardsCopy link
That second option is the part I actually built this for. Rollback isn't a separate emergency procedure with its own script and its own nervous rehearsal. It's the release workflow again, with an older version identifier typed into the box.
Which means a rollback rebuilds nothing. No fresh compile, no dependency resolution that might behave differently today than it did on Tuesday, no chance of the rollback shipping something subtly unlike what was running an hour ago. The artefact that gets promoted is the same artefact, byte for byte, that was built and tested and looked at. The first real release I ran under the new model went end to end in thirty three seconds, and it was that quick because promoting is just pointing traffic at something that already exists.
I've worked in plenty of places where the rollback plan was a document. Documents don't run at 2am.
I don't get a bypass eitherCopy link
All three repositories now carry a branch ruleset on main. Pull request required, the test check required and required to be current, force pushes blocked, deletions blocked. Ordinary stuff. The part worth mentioning is the bypass list, which is empty.
GitHub will happily let you put yourself on that list. Repository owner, administrator, one tick and the rules stop applying to you. I've left myself off, on all three.
A gate the owner can step round is not a gate. And I know exactly what I'm like at 11pm with a one-line fix that's obviously fine. If a genuine emergency ever turns up I can add a bypass, deliberately, and it'll be a decision I made rather than a habit I already had. The sanctioned fast route is a revert pull request, merged, then released, and that runs in about the same thirty seconds as anything else.
What I'm not going to pretendCopy link
One caveat, because the alternative is somebody finding it out the hard way. The staged preview on the members portal is a preview, not a sandbox. A Workers version shares the bindings of the Worker it belongs to, so that preview URL is talking to the production database and the production secrets. If a staged build writes a row, it writes it to the real database. That's still a large improvement on what it replaced, which had no look-first step at all. But nobody should read the word "preview" and hear "safe to poke about in". I know which of my three previews I can be careless in, and it isn't that one.
A smaller change fell out of the work and I liked it more than I expected to. The apps ping search engines when content changes, to invite a re-crawl. That ping used to fire on merge. Under the new model a merge publishes nothing, so a merge-time ping is announcing a page no reader can reach yet. It moved to release, where it belongs. Tiny thing, but a good example of what gets fudged when merge and publish are the same event.
Why bother, at the size of one blokeCopy link
I said at the top that this ceremony usually isn't worth it for one person, and I meant it. What changed my mind wasn't discipline.
I review, I steer, I say yes. But when a human wrote every line, review happened at the pull request, because that's where the human was. The diff was a fair proxy for the change, because a person had held the whole change in their head in order to write it. When agents write most of the lines and merge them the moment the tests go green, the pull request stops being where my judgement lands. It moves later, to the built thing, running, on a URL I can open.
That's why the staging step is worth having. It gives my review somewhere to stand. And the release step gives that review some teeth, because until I say the word, whatever the agents have merged is sitting on a shelf being harmless.
Worth being straight about the mechanism: the gate is me. There's no approval widget, no environment protection rule, no second reviewer, because there is no second reviewer. It's a staged build I look at and an instruction I give. In a company of two hundred that would be a weak gate and I wouldn't defend it. At a company of one with a team of agents it's the strongest thing available, and it's a great deal stronger than what it replaced, which was merging on green and finding out from the live app.
The habit is the slow partCopy link
Pipelines change in a weekend. Habits don't. I've caught myself twice already opening the live app straight after a merge, waiting for something that was never going to happen. Given that was the entire point of the exercise, it's quite a nice way to be wrong.
None of this is clever, and I'd rather say so than dress it up. All I've done is take the boring version, standard in large organisations for decades and mostly done to me rather than by me, and point it at an estate of one person and a handful of agents.
Merge stages. A human releases. Rollback is the same button with an older number in it.
Merged is not live. I'd recommend it.
Barry.