In today’s software development world, semantic versioning (SemVer) has become the go-to way of handling version numbers. Instead of random or confusing version bumps, SemVer provides a clear, predictable, and meaningful structure to versions helping teams instantly understand whether a release introduces a bug fix, a new feature, or a breaking change.
When you combine this with Azure DevOps pipelines, you unlock a powerful automation workflow. Every build, release, or deployment can automatically generate the right version number, attach it to your artifacts, and push it all the way through your environments. This makes your releases not just consistent, but also traceable and auditable.
Understanding Semantic Versioning
Semantic Versioning (often called SemVer) uses a simple, three-part number format: MAJOR.MINOR.PATCH (for example, 2.1.3). Each part of the version number has a specific meaning:
- MAJOR: goes up when you make big changes that break compatibility with previous versions.
- MINOR: increases when you add new features that still work with the old versions.
- PATCH: updates when you fix bugs or make small improvements that don’t affect compatibility.
This structured approach provides clarity about what changed, predictability for upgrades, and automated dependency management.
Why Automate Versioning?
As teams grow and deployments become more frequent, manually managing versions quickly turns into a challenge. Automated semantic versioning helps solve this problem by bringing structure and reliability to the process.
Implementing Semantic Versioning in Azure DevOps Pipeline
Automating semantic versioning in Azure DevOps Pipelines makes version management easier and more reliable. Instead of manually updating version numbers, the pipeline handles it for you — ensuring every build and release gets a clear, consistent, and meaningful version. By applying SemVer rules directly, your pipeline can automatically bump versions based on the type of changes made, making it simpler to track updates and keep your CI/CD process smooth and efficient.
Step 1: Create the First Git Tag
- Go to your Azure DevOps project.
- Navigate to Repos → Tags.
- Click “New Tag”.
- Enter:
- Tag name:
0.0.0 - Message:
Initial version - Choose branch/commit: Select the commit you want to tag (usually
main).
- Tag name:
- Click Create.
Step 2: Create semantic-version.yaml file in .pipeline folder
- In your .pipeline folder go to steps folder (if not exist create one).
- Inside a steps folder create a semantic-version.yaml file.
- And Add the below content as it is.
- Understand the below script, click here.
steps:
- checkout: self
persistCredentials: true
- script: |
echo "Detecting latest valid Semantic Version tag..."
# Get valid semver tags only
tags=$(git tag -l | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | sort -V)
latestTag=$(echo "$tags" | tail -n 1)
if [ -z "$latestTag" ]; then
latestTag="0.0.0"
fi
echo "Latest tag: $latestTag"
version=$latestTag
IFS='.' read -r major minor patch <<< "$version"
branchName="$(Build.SourceBranch)"
echo "Current branch: $branchName"
if [[ "$branchName" == *"/fix/"* || "$branchName" == *"/bugfix/"* || "$branchName" == *"/hotfix/"* ]]; then
echo "Patch branch detected - incrementing patch version"
patch=$((patch + 1))
elif [[ "$branchName" == *"/feature/"* || "$branchName" == *"/feat/"* ]]; then
echo "Feature branch detected - incrementing minor version"
minor=$((minor + 1))
patch=0
elif [[ "$branchName" == *"/major/"* ]]; then
echo "Major branch detected - incrementing major version"
major=$((major + 1))
minor=0
patch=0
else
echo "No matching branch type found - defaulting to patch increment"
patch=$((patch + 1))
fi
newTag="$major.$minor.$patch"
echo "New version: $newTag"
echo "##vso[task.setvariable variable=ImageTag;isOutput=true]$newTag"
echo "Pushing tag $newTag..."
git tag "$newTag"
git push origin "$newTag"
displayName: "Generate Semantic Version Tag"
name: SetVersionStep 3: Add Script to Azure DevOps Pipeline
In your azure-pipelines.yml (or your main pipeline file), add a step to run the script after a successful build:
- stage: Versioning
displayName: "Generate Semantic Version"
jobs:
- job: TagVersion
displayName: "Compute Semantic Version"
steps:
- template: steps/semantic-version.yaml
parameters:
name: SetVersionNow add the below line as a variable to use this tag as you Docker image tag or in any other steps of your pipeline.
variables:
IMAGE_TAG: $[stageDependencies.Versioning.TagVersion.outputs['SetVersion.ImageTag']]Step 4: Branch Naming Conventions
The script uses branch names to determine version bumps:
- fix/, bugfix/, hotfix/ →
PATCHincrement (e.g.,1.2.3 → 1.2.4) - feature/, feat/ →
MINORincrement (e.g.,1.2.3 → 1.3.0) - major/ →
MAJORincrement (e.g.,1.2.3 → 2.0.0)
So, when creating branches, stick to these conventions.
Step 5: Verify Tags in Azure Repos
After the pipeline runs, check the Tags section in your Azure Repo. You should see automatically created tags like 1.0.0, 1.1.0, 1.1.1, etc.
Semantic Versioning Script Explained
The following Bash script is used in the Azure DevOps pipeline to automatically generate the next semantic version tag based on the branch name and here is the detailed Explanation of the the script.
1. Get the Latest Semantic Version Tag
tags=$(git tag -l | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | sort -V)
latestTag=$(echo "$tags" | tail -n 1)
if [ -z "$latestTag" ]; then
latestTag="0.0.0"
fi
echo "Latest tag: $latestTag"git tag -l→ lists all tags in the repository.grep -E '^[0-9]+.[0-9]+.[0-9]+$'→ filters only valid SemVer tags (like1.2.3)sort -V→ sorts them in version order (so1.10.0comes after1.2.0)tail -n 1→ gets the latest version tag.- If no tag exists, we start from
0.0.0.
2. Split the Version into Major, Minor, and Patch
version=$latestTag
IFS='.' read -r major minor patch <<< "$version"IFS='.'→ sets the delimiter as.read -r major minor patch→ splits the version string into variables.- Example:
1.2.3→major=1,minor=2,patch=3.
- Example:
3. Get the Current Branch Name
branchName="$(Build.SourceBranch)"
echo "Current branch: $branchName"$(Build.SourceBranch)→ Azure DevOps variable that contains the branch name (e.g.,refs/heads/feature/add-login).- This determines which part of the version number to bump.
4. Decide Which Version to Increment
if [[ "$branchName" == *"/fix/"* || "$branchName" == *"/bugfix/"* || "$branchName" == *"/hotfix/"* ]]; then
echo "Patch branch detected - incrementing patch version"
patch=$((patch + 1))
elif [[ "$branchName" == *"/feature/"* || "$branchName" == *"/feat/"* ]]; then
echo "Feature branch detected - incrementing minor version"
minor=$((minor + 1))
patch=0
elif [[ "$branchName" == *"/major/"* ]]; then
echo "Major branch detected - incrementing major version"
major=$((major + 1))
minor=0
patch=0
else
echo "No matching branch type found - defaulting to patch increment"
patch=$((patch + 1))
fi- If the branch contains fix/bugfix/hotfix → bump the patch version (
1.2.3 → 1.2.4). - If the branch contains feature/feat → bump the minor version (
1.2.3 → 1.3.0). - If the branch contains major → bump the major version (
1.2.3 → 2.0.0). - Otherwise → default to patch increment.
5. Build the New Tag
newTag="$major.$minor.$patch"
echo "New version: $newTag"- Combines the updated version numbers into the new semantic version tag.
6. Create and Push the Tag
git tag "$newTag"
git push origin "$newTag"git tag "$newTag"→ creates the new tag locally.git push origin "$newTag"→ pushes it back to Azure Repos, making it available for everyone.
Conclusion
In the end, adding semantic versioning to your Azure DevOps pipeline isn’t just about following a standard, it’s about making life easier for your team and anyone using your software. With clear version numbers that reflect the type of change, you remove guesswork, build trust in your releases and keep deployments smooth and predictable. It’s a small step in setup, but it pays off big in long-term clarity and confidence.