A Beginner's Guide to Semantic Versioning

A Beginner's Guide to Semantic Versioning

July 9, 2023
An image showing the breakdown of each Semantic Version number

Semantic Versioning (SemVer) is a small set of rules for choosing version numbers. Many developers find it confusing at first, especially around when to bump which number and what release candidates are for. It helps to start with the problem it solves.

Purpose of Semantic Versioning

A version number is a message to the people who depend on your software. Each new number is trying to tell them something: does this release contain breaking changes, new features that don’t break anything, or just bug fixes? Should I update my dependency on it?

When and Why to Use Semantic Versioning

SemVer matters most when other people depend on your code: libraries, frameworks, and services with an API. It lets them decide whether a new release is safe to pick up without reading every commit.

How to Use Semantic Versioning

Semantic Versioning uses a three-part version number, MAJOR.MINOR.PATCH, and each part answers a different question:

  • MAJOR: Incremented when making incompatible changes or introducing breaking API changes.
  • MINOR: Incremented when adding new features or functionality in a backward-compatible manner.
  • PATCH: Incremented for backward-compatible bug fixes or patches.

When assigning version numbers, follow these guidelines:

  1. Start with version 0.1.0 for initial development. During the 0.y.z phase, anything may change at any time and the API should not be considered stable. Once you have a stable public API, release version 1.0.0.
  2. Are there any breaking changes? If yes, increment the MAJOR version by 1 and reset MINOR and PATCH to 0 (e.g., 1.2.32.0.0).
  3. Are there any new features that are backward-compatible and don’t require code changes for systems that depend on your API? If yes, increment the MINOR version by 1 and reset PATCH to 0 (e.g., 1.2.31.3.0).
  4. Are there bug fixes that are backward-compatible and don’t require code changes for systems that depend on your API? If yes, increment the PATCH version by 1 (e.g., 1.2.31.2.4).

Creating Release Candidates

Release Candidates (RCs) depend on your branching and release strategies. This is just an example of a branching/release strategy, by no means is this the only way to do it and it probably doesn’t make sense for your own use case. The important thing is to understand the concepts and apply them to your own needs. If you do not need release candidates, you can skip this section.
An image representing a simplified view of the release process, from test, to staging, and to production.

How you use release candidates depends on your branching and release strategy. To make it concrete, let us assume the following:

  • main branch is the mainline branch where all changes are merged into.
  • Work is done on feature branches, and merged into main via Pull Requests.
  • As soon as code gets merged to main, it is deployed to a test environment. At this point they are not tagged with a version number, and we will refer to them by their commit hash.
  • Release Candidates are created from main and deployed to a staging environment. This is done after it passes all the checks on test.
  • Release Candidates are tested and validated in the staging environment. If any issues are found, they are fixed on the main branch and a new Release Candidate is created, otherwise it is promoted to a Release.

A release candidate (RC) is a pre-release version that gets tested before it becomes the final release. SemVer does not require them, and they don’t fit every workflow, more on that later.

To create a release candidate, we typically follow these steps:

  1. Create a new branch or use an existing branch dedicated to the release.
  2. Apply the necessary changes or bug fixes on the release branch.
  3. Assign a unique identifier to the release candidate using the format X.Y.Z-rc.N, such as 1.2.0-rc.1, 1.2.0-rc.2, and so on. The hyphen prefix is part of the official SemVer specification for pre-release identifiers.
  4. Publish the release candidate for testing and gather feedback from users or testers.
  5. Continuously address any issues or bugs discovered during the testing phase.
  6. Repeat until the release candidate is stable and free of critical issues.

Sometimes shipping fast matters more than shipping safe, and the release candidate phase gets skipped.

It’s important to note that when a hotfix occurs, any release candidate in staging becomes outdated. In such cases, it is necessary to backmerge the changes from the outdated release candidate and create a new release candidate with an updated version number. This ensures that the hotfix and all other changes are properly incorporated into the production release.

When not to use release candidates

Release candidates only make sense if changes go through a validation step before reaching users. Plenty of setups don’t need that, for example:

  • Continuous deployment: If every merge is deployed automatically, there is no separate release to test. The testing happens in the pipeline instead.

  • Experimental features or prototypes: When stability doesn’t matter yet, a separate branch or tag is enough.

  • Small or personal projects: With few users, the overhead of managing release candidates outweighs the benefit.

  • Internal-only releases: If nobody outside your organization depends on the software, you can usually test it in a controlled environment and deploy directly.

Transition from Release Candidates to Release Versions

Once a release candidate has passed testing and no showstoppers remain, it becomes the official release. To promote it:

  1. Review the feedback from users or testers and address any remaining issues. Sometimes the only tester is the developer who created the RC. It is still worth validating before deploying.
  2. Pick the new version number based on the changes since the last release: MAJOR for breaking changes, MINOR for new features, PATCH for bug fixes.
  3. Tag the commit that represents the release candidate with the new version number.
  4. Tell users and stakeholders about the release and its version number.
An image representing the branching strategy exemplified in this post

When multiple changes exist, it is important to identify the highest level of change among them. For example, if a release candidate contains a bug fix and a new feature, the release version should be incremented as a MINOR version. This is because a MINOR version change indicates the addition of new features, which is a higher level of change than a bug fix.

Automating release versioning with GitHub Actions

Picking version numbers by hand is error-prone, so it’s worth automating. With GitHub Actions, a typical workflow looks like this:

  1. Trigger on pushes or merges to your release branch.
  2. Work out the bump by analyzing the changes since the last release. There are open source actions for this, usually based on commit message conventions.
  3. Update the version in the relevant files.
  4. Commit and tag the release.
  5. Publish the release notes and artifacts.

Conclusion

Semantic Versioning is a small set of rules, but it gives everyone who depends on your software a clear signal about what changed and whether it is safe to upgrade. That is worth the discipline.