Skip to content

Semantic Versioning (SemVer)

You've seen version numbers everywhere: 1.0.0, 2.4.1, 4.0.0-beta.2. But how does it work? How do you know if updating a dependency from 2.1.5 to 3.0.0 will break your entire application?

Semantic Versioning (SemVer) is the answer. It's a universal standard that tells a clear story about what kind of changes are in a new release.

The numbers, Mason! What do they mean?

A SemVer number is broken into three main parts. Once you understand them, you can understand the impact of any version change at a glance.

💥 MAJOR version

The Rule: You increment the MAJOR version when you make incompatible, breaking API changes.

When to use it: A function was removed, an endpoint changed its response, or anything else that would force users of your code to change theirs.

Example: Changing from 1.5.2 to 2.0.0 signals danger. Update with caution!

✨ MINOR version

The Rule: You increment the MINOR version when you add new functionality in a backward-compatible way.

When to use it: You added a new feature, a new optional parameter, or a new endpoint without changing existing ones.

Example: Changing from 2.0.0 to 2.1.0 adds cool new stuff without breaking the old stuff.

🐛 PATCH version

The Rule: You increment the PATCH version when you make backward-compatible bug fixes.

When to use it: You fixed something that was broken but didn't change any features. This is common for hotfixes.

Example: Changing from 2.1.0 to 2.1.1 is a safe and recommended bug fix.

The "Initial Development" Phase

Before your first official release, your MAJOR version should be 0. The version 0.y.z indicates that the project is in its early stages and anything can change at any time. We start with 1.0.0 once the application is deployed to production for the first time.

This way, you don't go to prod for the first time with a v2397.0.0

Anatomy of Our Full Version String

The MAJOR.MINOR.PATCH numbers are just the core. A full version string gives us even more detail.

Anatomy of a SemVer string

Our full version string has three parts: f

  1. Core Version: MAJOR.MINOR.PATCH as described above.
  2. Pre-release Tag: A label that qualifies the stability of the build. In our workflow, this tag is determined by the branch:
    • branch-name: Indicates an in-development build from a feature or bugfix branch. Highly unstable.
    • -beta: A build from the main branch. All features are complete, but it's not yet validated for production.
    • -rc (Release Candidate): A build from a support/* branch. This is a candidate for a production release and is undergoing final verification.
    • (no tag): A full production release, tagged and deployed.
  3. Build Number: An ever-increasing number for each build or commit on a pre-release version, in-between "stable" builds, providing uniqueness.

So, a version like 1.1.0-beta.1 means: "This is the first build of a beta release to prepare for version 1.1.0."

How Versions Are Bumped Automatically

So how do we get the right version number every time?

Our tooling (specifically, a tool like GitVersion) automatically calculates the next version by looking at your commit and the branch you're on. Everytime you will commit, merge, tag, GitVersion will compute the right SemVer for you. You can even use conventional commits to automatically bump the right digit depending on the type of commit.

Example Scenario

Let's see it in action. The last production release was tagged 1.0.0.

  1. A developer merges a bug fix: fix(GG-2): my fix.
    • The version on main becomes 1.0.1-beta.1. (Patch bump, first beta build)
  2. Another developer merges a second fix: fix(GG-3): my second fix.
    • The version on main becomes 1.0.1-beta.2. (Build number increases)
  3. A new feature is merged: feat(GG-4): my feature.
    • The version on main becomes 1.1.0-beta.3. (Minor bump overrides the patch, build number continues)
  4. Another new feature is merged: feat(GG-5): my feature.
    • The version on main becomes 1.1.0-beta.4. (Build number increases)
  5. A breaking change is merged: feat(GG-6)!: new auth system.
    • The version on main becomes 2.0.0-beta.5. (Major bump overrides everything, build number continues)
  6. Finally, we tag the previous commit "2.0.0", and we merge a new feature feat(GG-7).
    • The version on main becomes 2.1.0-beta.1. (Minor bump because it was a feat-typed commit. Build number got reset with the latest tag).

This system ensures our version numbers are always predictable, meaningful, and generated without any manual work. It's the engine that drives the entire workflow.

For all the details, visit the official Semantic Versioning specification.