Release Strategies
Every now and then I run into a discussion about how releases should be done: how to tag them, which git workflow to use, how often to ship. I would like to share how I do releases, and how I like to see a project do releases as a user of its software.
Releases
What is a release? A release is a named snapshot of a development cycle — a version. If you develop software, you should provide versions, because a version is how you communicate with your users. It tells them what to expect: what changed, what was fixed, what was added.
Versioning
There are essentially two strategies. A date-based scheme such as YYMMDD.PATCH, or semantic versioning with its MAJOR.MINOR.PATCH scheme. Both are fine. I use semver, because it is the more flexible of the two and because it makes the decision of which number to increment an objective one.
One thing that trips people up: these are integers separated by dots, not decimal fractions.
- As versions:
1.10is newer than1.9 - As decimals:
1.9is greater than1.10
Branches and Workflows
If you expect me to recommend the famous git flow model, I have to disappoint you. It did not work for me, and it caused more problems than it solved.
The worst of them was hotfixes. In that model a hotfix branches off master and then has to be merged into both master and the development branch. In a project under heavy development those two branches have diverged substantially by the time you need the hotfix, so what you get is a conflict that you resolve by writing the patch a second time, by hand. At which point the branching model has bought you nothing.
The Release Branch Model
The model I have found simplest and most successful is release branches.
There is one development branch: master. Every new feature branches off master into a feature/ branch, every fix into a fix/ branch, and both are merged back into master when they are done. A release gets a tag on master and a branch off that tag. Patch releases happen on the release branch.
branch:master
|
| tag:1.0.0
|\
| | branch:release-1.0
| | tag:1.0.1
| | tag:1.0.2
| | tag:1.0.3
|
|
| tag:1.1.0-rc.1
| tag:1.1.0-rc.2
| tag:1.1.0
|\
| | branch:release-1.1
| | tag:1.1.1
| | tag:1.1.2
| | tag:1.1.3
| | tag:1.1.4
What you can see above:
- One development branch, master.
- New releases are tagged on master — 1.0.0, 1.1.0 — and each tag gets a branch of its own,
release-1.0. - Patch releases such as 1.0.1 and 1.0.2 are made on the release branch.
A fix still has to reach master, but in contrast to git flow it is up to you how: cherry-pick it, merge it, or write a different fix if master has moved on. Nothing in the model forces a merge that is going to conflict.
Release candidates, alphas and betas are tagged on master as well, but they do not get a branch.
This is, more or less, how Linux kernel development works.
Maintaining Old Releases
Which brings up the important question: what happens to a release when the next one comes out? The only honest answer is that it depends.
As a developer you rarely want to maintain old releases forever, and a common policy is to support a release until two newer ones exist. From the other side of the fence it looks different: a new major release means new features, which means new code, which means bugs nobody has found yet. A conservative user wants the version with the fewest bugs, not the most features.
And depending on the software, expecting users to always follow the latest release is simply unrealistic — development frameworks, operating system distributions, the kernel, cloud platforms. Those users need a version that is maintained for a predictable period of time: a long term support release.
Long Term Support (LTS)
An LTS release is a policy, not a technical artefact: a commitment to backport fixes into a given version for a defined period of time.
Why LTS?
Because it lets you serve two groups of users that want opposite things:
- Users who want features, and want them as early as possible.
- Users who want the most stable software they can get.
Users who want features: mainline
If your software is under active development, you want many releases in a short period so that users get features quickly. The flip side is that those users have to keep up: they are forced to upgrade in order to receive fixes. That kind of rolling upgrade works well for non-critical software — browsers are the obvious example — and badly for anything running a power plant.
Users who want stability: LTS
The only software without bugs is software without code. Less code means less functionality, less complexity and fewer bugs; more features mean more complexity and more bugs. So the most stable build of your software is the one with the fewest features, tested for the longest time, and therefore carrying the largest number of fixes relative to its surface.
That is what an LTS release is. It gets no new features, only bug fixes — and since even a bug fix can introduce a new bug, an LTS release should only receive fixes that are worth the risk:
- the fix must matter
- the fix must be small and obvious
- the fix must already be in mainline
That last rule is the one people skip, and it is the one that keeps an LTS branch from drifting into being its own unmaintained fork.
Examples of LTS
- Ubuntu: 5 years of standard support per LTS release
- The Linux kernel: several years per LTS series
- Laravel: 3 years of bug fixes for an LTS release
What LTS Is Not
LTS is not “forever”. Software that does not change is dead software. Even Linux 2.6.32, released on 3 December 2009 and maintained as a long term release for over six years, eventually got its final update and was closed.
Release Scheduling
The most underrated part of releasing is how often you do it. There are roughly three patterns:
- whenever it happens — two releases in one week, then nothing for four months
- every one to three months
- once or twice a year
Which would you rather use? And which would you rather develop against?
As a developer
I want to know when my feature will ship.
- With one or two releases a year, the feature has to be finished and merged before the next release, or it waits half a year.
- With releases whenever they happen, I cannot tell when my merged feature will reach users at all.
- With a release every one to three months, an unfinished feature simply goes into the next one. No rush, no drama.
As a user
Am I really the only one who plans and tests upgrades?
- Upgrades have to be scheduled, for yourself or for customers.
- Releases have to be tested — users test releases, developers test snapshots.
- Knowing when a feature will land in your environment is worth a great deal.
Frequency
Frequency is the key, more than the interval itself. Release on a predictable cadence and your users develop a feel for the project and trust that the feature they are waiting for will arrive. Whether that cadence is one month or six matters less than that it exists — although I would not stretch it much beyond six months. Developers get the same benefit: they know when their work ships, so they can finish it properly and aim for the next train instead of the current one.
I hope this helps some of you plan better release cycles.
This post is from 2016 and the opinions in it have aged better than some of the examples.
The author of the git flow model added a note of his own in 2020, recommending simpler workflows such as GitHub Flow for software that is continuously delivered, and reserving his model for versioned software with multiple supported releases — which is roughly the distinction drawn above. Trunk-based development has meanwhile become the mainstream default, and the date-based scheme mentioned here is now generally known as CalVer.
The support windows have moved too. Ubuntu LTS releases still get 5 years of standard support, now extendable to 10 or more through Ubuntu Pro. The kernel project cut the projected support window for new LTS series to two years in 2023, having found that almost nobody was using the later years of the longer ones. Laravel dropped the LTS designation after version 6; each major release now gets about 18 months of bug fixes and 2 years of security fixes. And Linux 2.6.32 received its actual final update, 2.6.32.71, in March 2016 — a few weeks after this post was written.