Skip to content

Automated Semantic Versioning Made Easy

Every project needs a solid versioning strategy. I've been using GitVersion for a while now to keep things consistent across the board. In this post, I'll walk you through how I've hooked it into both GitHub and Azure DevOps, so you can easily drop it into your own setup, if you would be so inclined.

Techstack

The following technologies are described in this article:

  • Gitversion 6+
  • Azure DevOps pipelines
  • Github Actions

What is GitVersion?

GitVersion is an open-source tool that automatically calculates semantic version numbers for your project based on your Git history. Instead of manually deciding what version number to use for each release, GitVersion analyzes your branch structure, commit messages, and tags to determine the appropriate version increment.

Think of it as your versioning autopilot. It follows semantic versioning (SemVer) principles, where versions follow the MAJOR.MINOR.PATCH format:

  • MAJOR version for breaking changes that aren't backward compatible
  • MINOR version for new features that maintain backward compatibility
  • PATCH version for bug fixes and small improvements

Incrementing the version

GitVersion gives you fine-grained control over version bumping through commit message conventions. Instead of relying solely on branch patterns, you can explicitly mention what kind of version bump you want right in your commit message. When making a commit the message is scanned for specific keywords which will adjust the version accordingly.

The Magic Keywords

GitVersion recognizes these patterns in your commit messages by default:

  • +semver: major or +semver: breaking - Bumps the major version (1.0.0 → 2.0.0)
  • +semver: minor or +semver: feature - Bumps the minor version (1.0.0 → 1.1.0)
  • +semver: patch or +semver: fix - Bumps the patch version (1.0.0 → 1.0.1)
  • +semver: none or +semver: skip - No version bump at all

However, since I prefer using Conventional Commits I modified the configuration file to allow for using the keywords below instead.

bash
# Major bump
git commit -m "BREAKING CHANGE: restructure user authentication"

# Minor bump
git commit -m "feat: add dark mode support"

# Patch bump
git commit -m "fix/perf/refactor/revert: resolve memory leak in data processing"

# no bump
git commit -m "docs/chore/style/test/ci: update installation guide +semver: none"

Configuration

GitVersion allows you to customize its behaviour in many ways by using the configuration file.

Below I'll share my config file, which I've configured with some additional behavior that suites trunk-based development. I wanted to have some additional flexibility in versioning. Allowing me to start a release branch with a specific version number when needed, this way I can lock in a version on a branch that might live for a while before merging back to main.

Workflow and strategies

The workflow setting determines the base branching model, while strategies define how GitVersion should calculate versions.

yaml
workflow: GitHubFlow/v1

strategies:
  - MergeMessage
  - TaggedCommit
  - TrackReleaseBranches
  - VersionInBranchName
  
major-version-bump-message: '(BREAKING CHANGE|^[a-z]+(\([^)]+\))?!: )'
minor-version-bump-message: '^feat(\([^)]+\))?: '
patch-version-bump-message: '^(fix|perf|refactor|revert)(\([^)]+\))?: '
no-bump-message: '^(chore|docs|style|test|ci)(\([^)]+\))?: '
commit-message-incrementing: MergeMessageOnly # Only commit to main message is taking into account, history in PR is ignored. Prefered for trunk based

I'm using GitHubFlow/v1 which works well for trunk-based development. The strategies tell GitVersion to look at merge messages, tagged commits, release branches, and version numbers in branch names when calculating the version.

Conventional Commits

The commit message patterns are set to match Conventional Commits, allowing for automatic version bumps based on the type of change.

yaml
workflow: GitHubFlow/v1 

strategies:
  - MergeMessage
  - TaggedCommit
  - TrackReleaseBranches
  - VersionInBranchName
  
major-version-bump-message: '(BREAKING CHANGE|^[a-z]+(\([^)]+\))?!: )'
minor-version-bump-message: '^feat(\([^)]+\))?: '
patch-version-bump-message: '^(fix|perf|refactor|revert)(\([^)]+\))?: '
no-bump-message: '^(chore|docs|style|test|ci)(\([^)]+\))?: '
commit-message-incrementing: MergeMessageOnly

by setting commit-message-incrementing: MergeMessageOnly, only the merge commit message to main is considered for version bumps, ignoring the individual commits in the pull request. This is ideal for trunk-based development where you want to control version increments at the point of merging. you can omit this line if you want to consider all commit messages on the release branch to determine the final version.

Main branch

The main branch is where my stable code lives. This configuration ensures proper version increments on every merge.

yml
workflow: GitHubFlow/v1

branches: 
  main:
    regex: ^master$|^main$
    increment: Patch
    prevent-increment:
      of-merged-branch: true
    track-merge-target: false
    track-merge-message: true
    is-main-branch: true
    mode: ContinuousDeployment

By default, main increments the patch version. The track-merge-message: true setting means GitVersion will look at merge commit messages to determine if a minor or major bump is needed (using the Conventional Commits patterns we configured earlier). The ContinuousDeployment mode ensures clean version numbers without pre-release suffixes. If you prefer to have a pre-release suffix on each commit, you can switch to ContinuousDelivery mode.

Release branch

TIP

This configuration requires an existing semantic tag on the repository when onboarding for the first time.

This is where things get interesting. The release branch setup allows us to lock in specific versions.

yml
workflow: GitHubFlow/v1

major-version-bump-message: '(BREAKING CHANGE|^[a-z]+(\([^)]+\))?!: )'
minor-version-bump-message: '^feat(\([^)]+\))?: '
patch-version-bump-message: '^(fix|perf|refactor|revert)(\([^)]+\))?: '
no-bump-message: '^(chore|docs|style|test|ci)(\([^)]+\))?: '
commit-message-incrementing: MergeMessageOnly

strategies:
  - MergeMessage
  - TaggedCommit
  - TrackReleaseBranches
  - VersionInBranchName
branches:
  main:
    regex: ^master$|^main$
    increment: Patch
    prevent-increment:
      of-merged-branch: true
    track-merge-target: false
    track-merge-message: true
    is-main-branch: true
    mode: ContinuousDeployment
  release: 
    regex: ^release/(?<BranchName>[0-9]+\.[0-9]+\.[0-9]+)$
    label: ''
    increment: None
    prevent-increment:
      when-current-commit-tagged: true
      of-merged-branch: true
    is-release-branch: true
    mode: ContinuousDeployment 
    source-branches:
      - main

The regex captures the version number from the branch name (like release/0.2.0). Setting increment: None means the version won't auto-increment, it stays exactly what you specified in the branch name. This is perfect when you need precise control over release versioning.

Usage

TIP

Check your configuration with /showconfig to ensure everything is set up correctly.

Run the binary in your repository to see the calculated version and environment variables.

bash
gitversion

Below is an example output from using my configuration above.

GitVersion Output Example

Integrate GitVersion into your workflow

GitVersion integrates seamlessly into CI/CD pipelines, ensuring consistent versioning across different environments and teams. It eliminates the guesswork and human error that often comes with manual version management.

In this section I'll show you how I integrated GitVersion into GitHub Actions and Azure DevOps Pipelines.

Github

The GitHub marketplace lets you easily share custom actions. Check out the action repo to see how to use GitVersion with GitHub actions, feel free to fork or clone this action and tweak it to your needs. It's built to serve as a solid foundation for anyone to build on.

features:

  • Tags the repo with the calculated SemVer and output the value for usage in subsequent steps.
  • Annotates the tag with the latest commit message.
  • Allows specifying a custom path for the configuration file.

Azure DevOps

For Azure DevOps, I'll share a pipeline template snippet that you can incorporate into your pipelines after setting it up. The general steps are the same but the specifics of the templating in the platforms are of course handled a bit different.