I have worked as a software engineer at companies of different sizes. While their development processes were often quite similar (thanks to the Agile manifesto :D) their approaches to releases varied a lot. Some teams had almost no release process at all, while others defined something so complex it required multiple wiki pages to explain. In this post, I will try to come up with a simple release process for small teams (<20), based on what I have seen work in practice.

Why focus on something non scalable? Well, not everything needs to scale :D

What is a release?

A release is an immutable, labeled snapshot of something. In our case, that something is a piece of software. One key property of a release is that its source must be retrievable. At any point in the future, you should be able to go back and inspect the exact state it represents.

Stage 0: Snapshots

Before a release is created, people sometimes like to create snapshots. I say sometimes because they don’t always make sense. If you are not going to test or integrate the snapshot somewhere then there is no need to create it.

A snapshot should either be built after each git push event or on a nightly basis but not together!

Stage 1: Preparing the release

Once the decision to make a release is taken, the release needs to be prepared.

Each release should either get its own changelog file or a dedicated section in a changelog file. This is very important for everyone, who did not work on the release, to know what has changed. The same applies to migration guides.

Next, set the release version in all relevant files (e.g. pom.xml, Cargo.toml, version.txt etc.). Developers like to automate this part with CI / CD pipelines but it is crucial that this is done explicitly at this stage. If the version is set during the pipeline execution, the release becomes less predictable. Until the repository is cloned, you cannot be sure which exact state will be built. For example, you might trigger a build while a colleague pushes changes at the same time. When the pipeline eventually runs, it could pick up those new changes, even if they were not part of what you intended to release. The likelihood of this happening is low but it is not zero.

Stage 2: Trigering the release

Trigering a release should be fast and straight forward. No wiki pages needed! A git tag should be used as the trigger since that gives us the guarantee that the pipeline will use the exact state that we had in mind when trigering it.

Do not allow for more than one trigger. If you don’t like git tags then you can choose another method but don’t support both. The reason behind this is that if you allow people to do something in multiple ways, then they get confused and do mistakes. Even if they are smart engineers!

NOTE: Git tags are not really immutable. They can be changed after release. However, it needs a force-push and in small teams you should be able to trust your colleagues not to do that.

What happens if the pipeline fails?

Just fix it and either create a new tag or update the existing one. Updating the already created tag should be fine since the final release was not done yet.

What about trigering a RC?

Release candidates are also just releases. So the same rules apply here.

Stage 3: Release branches

After a release is done, no release branch should be created. They don’t provide any value if they contain the same state as the git tag. They should only be created when you are going to support updates (security patches, bugfixes) for that release.

Release branches should not be based on the main branch but on the release tag. Example command: git checkout -b release/1.x.x v1.0.0

Closing note

The release stages described above represent a general workflow. Depending on the project, this process may include additional stages or intermediate steps.