Manual version bumps using semantic release with Azure DevOps

Published: (December 28, 2025 at 04:43 PM EST)
5 min read
Source: Dev.to

Source: Dev.to

Cover image for Manual version bumps using semantic release with Azure DevOps

Daniel Marques

The semantic-release package (https://github.com/semantic-release/semantic-release) automates the version management and release process of a project. It determines the next version number based on the commit messages that adhere to the Conventional Commits specification, generates release notes, and publishes the release automatically.

However, some developers prefer more control over when to increment major and minor versions. Companies like JetBrains use the year as a major version and an auto‑incrementing integer as a minor version, as illustrated in the image below.

JetBrain versioning

Since semantic-release offers various built‑in tools, such as a release notes generator, it is interesting to keep using it and just change the bumping logic. That is what I will show in this post.

Azure DevOps pipeline for Manual bumping

My approach involves creating an Azure DevOps pipeline that runs semantic-release with the following steps:

  • The pipeline prompts the user to specify the bump type (major, minor, or patch) and the desired version number.
  • Once the user confirms the choice, if the user opted for a major or minor version, the pipeline transitions to an approval state.
  • Upon approval, the pipeline increments the version according to the chosen bump type.
  • The pipeline verifies that the bump aligns with the desired version number.

The code snippet below shows the Azure DevOps pipeline YAML with the steps above. The parameters are used to request the user’s bump type (default set to patch) and the wanted version. Their values are set as environment variables that are used in the semantic‑release configuration.

parameters:
  - name: bumpType
    type: string
    default: "patch"
    values:
      - major
      - minor
      - patch
  - name: bumpNumber
    type: string
    default: "0"

pool:
  vmImage: ubuntu-latest

jobs:
  - job: approval
    pool: server
    steps:
      - task: ManualValidation@1
        # …
        condition: ne('${{ parameters.bumpType }}', 'patch')

  - job: create_tag
    dependsOn: approval
    steps:
      # …
      - script: |
          npx semantic-release
        displayName: Run semantic-release setting ${{ parameters.bumpType }} version to ${{ parameters.bumpNumber }}
        env:
          SEMANTIC_RELEASE_BUMP_TYPE: ${{ parameters.bumpType }}
          SEMANTIC_RELEASE_BUMP_NUMBER: ${{ parameters.bumpNumber }}

That’s it! With this setup, you can manually bump the versions of your project without having to worry about commit messages.

The following snippet shows the semantic‑release configuration file (release.config.cjs). For the plugin @semantic-release/commit-analyzer (which normally bumps the version according to commit messages) we set it to always increase the version according to the bump‑type options chosen by the user.

// file release.config.cjs
module.exports = {
  // …
  plugins: [
    // …
    [
      '@semantic-release/commit-analyzer',
      {
        releaseRules: [
          { release: process.env.SEMANTIC_RELEASE_BUMP_TYPE }
        ]
      }
    ],
    './verify-release.js'
  ]
};

The verify-release.js plugin verifies if the new version is incremented as expected. This ensures that if the pipeline is executed with the same input a second time, it will fail because the bump would produce an undesired value (e.g., setting the major version to the next year in the JetBrains example). You can see the verify-release.js code in the next snippet.

// file verify-release.js
module.exports = {
  verifyRelease: async (pluginConfig, context) => {
    const { lastRelease = {}, nextRelease = {}, logger = console } = context;
    const bumpType = process.env.SEMANTIC_RELEASE_BUMP_TYPE;
    const bumpNumber = process.env.SEMANTIC_RELEASE_BUMP_NUMBER;

    logger.log('Verifying expected release.');
    if (!bumpType) {
      logger.log('SEMANTIC_RELEASE_BUMP_TYPE not set — skipping version verification.');
      return;
    }
    if (bumpType == 'patch') {
      logger.log('Bump type set to patch. Nothing to verify.');
      return;
    }

    const actual = nextRelease && nextRelease.version;
    const match = actual.match(/^(\d+)\.(\d+)\.\d+$/);

    if (!match) {
      throw new Error(`Invalid tag format: ${lastRelease}`);
    }

    const actualMajor = Number(match[1]);
    const actualMinor = Number(match[2]);

    if (bumpType == 'major' && actualMajor != bumpNumber) {
      logger.error(`Major version mismatch: expected ${bumpNumber} but will publish ${actualMajor}`);
      throw new Error(`Version verification failed: expected major version ${bumpNumber}, got ${actualMajor}`);
    }
    if (bumpType == 'minor' && actualMinor != bumpNumber) {
      logger.error(`Minor version mismatch: expected ${bumpNumber} but will publish ${actualMinor}`);
      throw new Error(`Version verification failed: expected minor version ${bumpNumber}, got ${actualMinor}`);
    }
  }
};

The picture below shows one example of pipeline execution that bumped the major version.

Pipeline execution – major version bump

Building a New Version

Once the release is ready, you can begin building your application. For this example, a simple Hello World CLI written in Go is used.

The pipeline’s steps are as follows:

  1. Checkout the code with tags
  2. Get a tag‑based description of the current commit – if the commit lacks a tag, a descriptive version based on the latest tag is created.
  3. Set Azure DevOps build number to the description from the previous step.
  4. Generate the CLI executable and publish it as a build artifact.

Azure Pipelines YAML

variables:
  appName: hello-world
  buildDir: build

steps:
  - checkout: self
    fetchTags: true
    fetchDepth: 0

  - script: |
      export VERSION=$(git describe --tags)
      echo "##vso[build.updatebuildnumber]${VERSION}"
    displayName: "Set build number"

  - task: GoTool@0
    inputs:
      version: "1.25"

  - script: |
      mkdir -p $(buildDir)
      go build -o $(buildDir)/$(appName) ./cmd
    displayName: "Build Go binary"

  - script: |
      cd $(buildDir)
      zip $(appName)-$(Build.BuildNumber).zip $(appName)
    displayName: "Create ZIP with version"

  - task: PublishBuildArtifacts@1
    inputs:
      PathtoPublish: "$(buildDir)"
      ArtifactName: "release"
      publishLocation: "Container"

The following image illustrates a successful build.

Successful build output

The code used in this post is available in the GitHub repository.

Back to Blog

Related posts

Read more »