×

Semantic Versioning in Azure DevOps: Automating Version Management with semantic-release-ado

Introduction

Managing software versions effectively is a critical aspect of the development lifecycle. It helps teams track changes, maintain backward compatibility, and communicate the impact of updates to users. In this article, we'll explore how to implement automated semantic versioning in Azure DevOps environments using semantic-release-ado, an adaptation of the popular semantic-release library originally designed for GitHub workflows.

Understanding Semantic Versioning

Semantic versioning (SemVer) is a versioning scheme that uses a 3-part version number: MAJOR.MINOR.PATCH (e.g., 2.4.1). Each component has a specific meaning:

  • MAJOR: Increased when making incompatible API changes
  • MINOR: Increased when adding functionality in a backward-compatible manner
  • PATCH: Increased when making backward-compatible bug fixes

This standardized approach enables developers and users to understand the impact of version changes at a glance.

The Goals of semantic-release

The semantic-release library was created to automate the version management and package publishing workflow. Its core goals include:

  • Fully automated version management: No manual intervention needed to determine the next version number
  • Enforcing semantic versioning: Ensuring version numbers adhere to SemVer conventions
  • Communicating changes effectively: Automatically generating changelogs
  • Triggering deployment processes: Seamlessly integrating with CI/CD pipelines
  • Reducing maintenance overhead: Eliminating version-related boilerplate and human error

However, semantic-release was primarily designed for GitHub environments and npm packages. For teams using Azure DevOps, this presented challenges in adopting these automation benefits.

Introducing semantic-release-ado

This is where semantic-release-ado comes into play. This library extends the functionality of semantic-release to work seamlessly within Azure DevOps environments. With semantic-release-ado, teams can enjoy all the benefits of semantic-release while working with Azure DevOps repositories, build pipelines, and release workflows.

Key features of semantic-release-ado include:

  • Azure DevOps integration: Works natively with Azure DevOps repositories
  • Automatic version calculation: Analyzes commit history to determine the next version
  • Git tagging: Creates version tags in your repository
  • Changelog generation: Produces comprehensive changelogs based on commit history
  • Build pipeline integration: Seamlessly fits into your existing CI/CD pipeline

Benefits for Your Codebase and Releases

Implementing semantic-release-ado brings several tangible benefits:

Consistent Versioning

By automating the versioning process based on commit messages, your team ensures consistent application of semantic versioning principles. This eliminates debates about what the next version number should be and reduces the risk of incorrect versioning.

Enhanced Documentation

Automated changelog generation keeps your release notes up-to-date with minimal effort. Each release includes a comprehensive list of changes categorized by type (features, fixes, breaking changes) and linked to the relevant commits or work items.

Streamlined Workflow

With semantic-release-ado, version management becomes part of your CI/CD pipeline rather than a separate manual process. This streamlines your release workflow and eliminates version-related bottlenecks.

Better Communication

The semantic versioning scheme communicates the impact of changes to users through the version number itself. This helps maintainers and users make informed decisions about updating.

Implementation Guide

Let's walk through implementing semantic-release-ado in an Azure DevOps environment.

1. Install Dependencies

Your package.json should include these dependencies:

{
  "devDependencies": {
    "@commitlint/cli": "^18.5.0",
    "@commitlint/config-conventional": "^18.5.0",
    "@semantic-release/changelog": "^6.0.3",
    "@semantic-release/commit-analyzer": "^13.0.1",
    "@semantic-release/git": "^10.0.1",
    "@semantic-release/release-notes-generator": "^14.0.3",
    "commitizen": "^4.3.0",
    "cz-conventional-changelog": "^3.3.0",
    "husky": "^9.0.11",
    "semantic-release": "^24.2.3",
    "semantic-release-ado": "^1.4.0"
  }
}

Important Note: These recent versions of semantic-release and its plugins require Node.js v20.8.1 or higher. We recommend using Node.js v22.15.1 (LTS) or newer.

2. Configure semantic-release

Create a .releaserc.json file in your project root with the following configuration:

{
  "branches": ["main"],
  "plugins": [
    "@semantic-release/commit-analyzer",
    "@semantic-release/release-notes-generator",
    [
      "@semantic-release/changelog",
      {
        "changelogFile": "CHANGELOG.md",
        "changelogTitle": "# Semantic .NET Demo Changelog"
      }
    ],
    [
      "@semantic-release/git",
      {
        "assets": [
          "src/SemanticDotNetDemo.csproj",
          "package.json",
          "CHANGELOG.md"
        ],
        "message": "chore(release): ${nextRelease.version} [skip ci] [skip hook]\n\n${nextRelease.notes}"
      }
    ],
    [
      "semantic-release-ado",
      {
        "setOnlyOnRelease": false,
        "isOutput": true
      }
    ]
  ]
}

This configuration:

  • Uses commit-analyzer to determine version increments
  • Generates release notes
  • Updates the CHANGELOG.md file with a custom title
  • Commits changes to project files, package.json, and CHANGELOG.md
  • Sets up semantic-release-ado with:
    • setOnlyOnRelease: false — always set the version variable
    • isOutput: true — make the version available as an output variable

Important Note: Setting isOutput: true in the semantic-release-ado configuration serves 2 purposes:

  1. Makes the version number available as $(stepName.nextRelease) rather than just $(nextRelease), which is important for accessing the version in subsequent steps or jobs in your pipeline
  2. Avoids the bracket issue documented in Issue #29 where isOutput: false can cause Azure DevOps to prepend a bracket to the version number (e.g., ]1.2.3)

3. Configure Your Azure Pipeline

Here's how to integrate semantic-release into your Azure DevOps pipeline:

trigger:
  - main

pool:
  vmImage: "ubuntu-latest"

variables:
  buildConfiguration: "Release"

steps:
  # Setup .NET Core SDK
  - task: UseDotNet@2
    inputs:
      packageType: "sdk"
      version: "9.0.x"
      installationPath: $(Agent.ToolsDirectory)/dotnet

  # Setup Node.js environment for semantic-release
  - task: NodeTool@0
    inputs:
      versionSpec: "22.x" # Newer semantic-release requires Node.js v20.8.1+
    displayName: "Install Node.js"

  # Checkout with credentials persisted for semantic-release
  - checkout: self
    persistCredentials: true

  # Install Node.js dependencies
  - script: |
      npm ci
    displayName: "Install Node.js dependencies"

  # Run semantic-release to determine version
  - script: |
      # Run semantic-release - the plugin will automatically set $(nextRelease)
      npx semantic-release
    displayName: "Run semantic-release"    name: semanticStep
    env:
      GIT_CREDENTIALS: $(System.AccessToken)
      GH_TOKEN: $(System.AccessToken)

  # Note: By setting isOutput: true in the semantic-release-ado plugin configuration,
  # the version will be available as $(semanticStep.nextRelease) in subsequent steps

  # Restore and build the project
  - script: |
      dotnet restore src/SemanticDotNetDemo.csproj
      dotnet build src/SemanticDotNetDemo.csproj --configuration $(buildConfiguration)
    displayName: "Build .NET application"
  # Create NuGet package
  - script: |
      dotnet pack src/SemanticDotNetDemo.csproj --configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory) /p:Version=$(semanticStep.nextRelease)
    displayName: "Create NuGet package"
    condition: ne(variables['semanticStep.nextRelease'], '')
  # Publish artifacts
  - task: PublishBuildArtifacts@1
    inputs:
      PathtoPublish: "$(Build.ArtifactStagingDirectory)"
      ArtifactName: "nuget-packages"
      publishLocation: "Container"
    condition: ne(variables['semanticStep.nextRelease'], '')

4. Set Up Commit Message Validation

To enforce the Conventional Commits format, set up commitlint with Husky:

  1. Create a commitlint.config.js file:
    module.exports = {
      extends: ["@commitlint/config-conventional"],
      rules: {
        "body-max-line-length": [0, "always", Infinity],
      },
    };
  2. Initialize Husky and add the commit-msg hook:
    npx husky init
    npx husky add .husky/commit-msg "npx --no -- commitlint --edit \"$1\""
  3. Add Commitizen for an interactive commit message format helper:
    // package.json
    "config": {
      "commitizen": {
        "path": "./node_modules/cz-conventional-changelog"
      }
    }

Testing Your Setup

Before deploying to production, test your semantic-release configuration locally:

# Verify the semantic-release configuration
npx semantic-release --dry-run --no-ci

This will analyze your commit history and show what version would be released without actually creating tags or releases.

Viewing Generated Artifacts

After a successful run of semantic-release in your Azure DevOps pipeline, you'll see:

  1. Updated version numbers in your project files
  2. A new Git tag with the version number
  3. An updated CHANGELOG.md with the latest changes
  4. Azure DevOps variables set for the build pipeline

Version Number Output

There are a few important considerations when working with version numbers in semantic-release-ado:

Output Variable Configuration

When using semantic-release-ado, you have 2 options for accessing the version number:

  1. Simple Variable (setOnlyOnRelease: false):
    • Makes the version available as $(nextRelease)
    • More straightforward, but can have issues with variable persistence
    • May experience the leading bracket issue
  2. Output Variable (isOutput: true):
    • Makes the version available as $(stepName.nextRelease)
    • More reliable for multi-job or multi-stage pipelines
    • Recommended approach for complex pipelines

Leading Bracket Issue

There is a known issue where semantic-release-ado can sometimes output version numbers with a leading bracket (e.g., ]1.2.3 instead of 1.2.3). This occurs due to how Azure DevOps handles logging commands when certain variable attributes are undefined or set to false.

We recommend:

  • Using isOutput: true in your semantic-release-ado configuration
  • Referencing the version through the step output variable ($(stepName.nextRelease))
  • If needed, adding a cleaning step to remove any leading brackets:
    - script: |
        CLEAN_VERSION=$(echo "$(nextRelease)" | sed 's/^]//')
        echo "##vso[task.setvariable variable=nextRelease]$CLEAN_VERSION"

For more information about this issue, see semantic-release-ado Issue #29.

Conclusion

Implementing semantic-release-ado in your Azure DevOps environment brings the power of automated semantic versioning to your development workflow. By combining it with Conventional Commits, you create a seamless process that:

  • Automates version number determination
  • Generates comprehensive changelogs
  • Creates consistent Git tags
  • Enhances your release management process
  • Improves communication about changes

While semantic-release was originally designed for GitHub, semantic-release-ado now brings these same benefits to Azure DevOps users. By adopting this approach, your team can focus more on delivering value and less on managing the mechanics of releases and versioning.

Additional Resources

Chuck BryanCore Contributor

Chuck Bryan is the Director of Professional Development at Marathon Consulting, where he helps employees grow their careers through mentorship, training, and access to learning resources. He is an active leader in the company’s intern program and is passionate about supporting the next generation of tech talent, ensuring they’re equipped with both technical skills and professional confidence.

With over 25 years of experience as a software developer, Chuck specializes in building and modernizing line-of-business applications using .NET, Azure, and containerized deployments. He’s a strong advocate for clean architecture, testable code, and practical automation strategies that improve software delivery and maintainability.

Outside of work, Chuck is the creator and host of The Cinematic Flashback Podcast, a show that revisits and celebrates the films of the 1970s. He also enjoys traveling and maintaining an active lifestyle through Orange Theory workouts.