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.
# 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.
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.
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.
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.
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.
gitversion
Below is an example output from using my configuration above.
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.