Between 9:13 in the morning and 5:30 in the afternoon I cut six tagged production versions of a classic-ASP app that’s been running for twenty years: a minor release and five hotfixes on top of it. No CI gate. No pull requests. No staging soak. Each one merged straight to master, tagged, and pushed to a build server that deployed it.
If you came up through a shop with branch protection rules and a green-checkmark culture, that sentence reads like a confession. It isn’t. The thing that made six releases manageable that day had less to do with the number of gates in front of them than with what each tag carried and how cleanly it could be taken back.
The cadence
The day started with a minor release cut around 9:37. That’s the normal flow: develop gets a version wrap, it becomes master, it gets a plain version tag, the build server picks it up.
Then the train. Over the next several hours, five hotfix/ branches, each one cut from master, each carrying exactly one ticket, each merged back to master, tagged, and merged back into develop within minutes of being cut. The version numbers climbed one patch digit at a time.
The map is the whole point:
- hotfix 1 carried one ticket
- hotfix 2 carried one ticket
- hotfix 3 carried one ticket
- hotfix 4 carried a tightly-coupled pair
- hotfix 5 carried one ticket
Five hotfix tags, and you can name the ticket behind each one without opening the diff. That’s not an accident of the day. It’s the rule the whole train runs on.
One ticket per tag is a rollback strategy, not bookkeeping
Here’s the question that actually matters when you ship straight to production with no soak: when something breaks at 4pm, how fast can you get back to known-good, and how much working code do you destroy doing it?
Batch five fixes into one release and the answer is bad. The tag that’s now live contains five changes. One of them broke. To roll back you either revert the whole tag, which kills four good fixes that customers are already using, or you go spelunking in the diff to surgically pull out the one bad change while the site is degraded and you’re under pressure. Neither is fast and neither is clean.
One ticket per hotfix tag collapses that. The third hotfix broke? It was one ticket. The rollback is “revert to the tag before it,” which is one step back, which contains every other fix from the morning except the bad one. The rollback scope is bounded by that tag’s change set, because the version granularity is kept close to one ticket.
That’s the trade I keep coming back to: version granularity is your blast radius. Coarse versions mean coarse rollbacks. If you make your tags as small as your tickets, your worst-case recovery is as small as your tickets too. On a legacy app with uneven test coverage, shrinking the unit of rollback is especially valuable alongside the local verification already in the release process.
Cut from master, then merge back into develop
The branch choreography matters because the easy version of this is also the wrong version. The wrong version is: hotfix straight onto master, ship, and forget. Do that five times and develop has drifted five fixes and five version bumps behind production. Your next minor release either silently loses those fixes or fights a merge conflict over version metadata.
The discipline that keeps the two branches honest is cut from master, version-wrap, then merge the tag back into develop. Concretely, for each hotfix:
- Cut a
hotfix/branch from master. - Land the one ticket’s change.
- Merge to master, bump the version, tag it.
- Merge that same commit back into develop so develop carries both the fix and the version bump.
Skip step 4 and you’ve started a slow leak. Master and develop diverge on exactly the thing that’s hardest to reconcile later, the version number, and the next person to cut a release inherits a conflict they didn’t cause. The merge-back is the cheap insurance that keeps “ship from master” from poisoning “build from develop.”
Multiple hands on the same train without a collision
This wasn’t a solo morning. There were three of us on the codebase at once, and the way you interleave committers on a hotfix train without stepping on each other is that nobody works on a shared branch. Every change lives on a dedicated feature/ or hotfix/ branch named for its ticket. The branch name is the lock. If your change is on your branch and mine is on mine, we can both be merging to master in the same hour and the only place we meet is the merge itself, which git is good at telling us about.
The one-ticket-per-branch rule isn’t only a rollback convenience. It’s also how a small team avoids the merge-hell that “everybody on develop” produces, without anybody having to announce what they’re touching.
The honest part: the author is the QA
So where did the staging soak go? It didn’t disappear. It moved.
“No staging” is doing a lot of work in that opening paragraph, and I want to be straight about what’s actually behind it. Before any of those hotfixes got cut, the fix ran on a local IIS clone of the app and I demoed it working with my own eyes. Click the thing, watch it do the right thing, then cut the tag. The verification step is real. It’s just that the person writing the code is the person watching it work, on a clone that mirrors production, instead of a separate environment and a separate QA pass.
That’s the load-bearing assumption, and it’s worth naming because it’s exactly where this model stops scaling. It works because the author can fully exercise the change locally and trust what they see. The moment a fix has effects you can’t observe on a clone, a background job, a payment webhook, something that only misbehaves under real traffic, “the author is the QA” stops covering you and you’ve earned a soak whether you like it or not. For a UI tweak or a query fix you can drive by hand, the local demo is the soak. For anything you can’t see happen, it isn’t.
When not to run the train
The same day had a counter-example sitting right next to the hotfixes: a net-new analytics surface, several thousand lines across a clutch of new pages, all landed in a single feature branch that merged to develop and rode the next minor instead of getting its own hotfix tag. That’s correct. Hotfixes are for changes with production-behavior risk that you want to ship and roll back atomically. New pages that nothing else depends on have no existing behavior to break, so there’s nothing to roll back independently and no reason to spend a version number on it. It merges to develop and waits for the train, it doesn’t ride it.
The rule that falls out: hotfix-per-ticket is for changes that touch live behavior. Greenfield rides the minor. Knowing which bucket a change is in is most of knowing whether to cut it its own tag.
Related
- Seven production releases in one day: the case for tiny, tagged, revertable hotfixes: the same small-tag discipline on a different payment incident
- Route a Change by Its Blast Radius, Not Its Size or Your Schedule: the decision framework behind choosing a hotfix or a broader release unit