DEV Community

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

Posted on

Manual version bumps using semantic release with Azure DevOps

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 the semantic-release with the following steps:

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

The code snipet below shows the Azure Devops pipeline YAML with steps above. The parameters is used to request user bump type (default bump type set to patch) and wished version. Its values is set as environment variables that are used at 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 }

Enter fullscreen mode Exit fullscreen mode

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

The following code snipet, show the semantic release configuration plugin configuration at release.config.cjs. For the plugin @semantic-release/commit-analyzer (which bumps version according to commit messages) is set to always increase the version according to bump type options choosen 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'
  ]
};
Enter fullscreen mode Exit fullscreen mode

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 for the second time, it will fail because the bump will go to an undesired value (taking the JetBrains example, setting the major version to the next year). 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}`);
        }
        logger.log(`Version verification passed: ${actual}`);
      }
}
Enter fullscreen mode Exit fullscreen mode

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

Building new version

Once the release is ready, you can begin building your application. For this example, I’ve created a simple hello world CLI in Go.

The pipeline’s steps are as follows:

  1. Checkout the code with tags
  2. Gets a tag-based description of the current commit. If the commit lacks a tag, it creates a descriptive version based on the latest tag.
  3. Sets Azure DevOps build number to the description from previous step
  4. Generates cli executable and publishes it as build artifact.

Here’s a code snippet that shows the above pipeline:


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"

Enter fullscreen mode Exit fullscreen mode

The following image illustrates one successful build.

The code used in this post is available in the github repository https://github.com/dmo2000/semantic-release-manual

Top comments (0)