semver-utils: Streamlined semantic versioning from pipelines

Author: Derek Mortimer | Posted on: March 30, 2025

At CECG , a recurring task we run into is the management of semantic version tags in Git repositories from automated pipelines. It’s straightforward in principle — just follow the Semantic Versioning rules! But in practice, we often saw a range of implementations with varying degrees of testing. To address this task in a repeatable and reusable manner, we built semver-utils , which we’re releasing under open-source now.

What does it do?

semver-utils comes with two binaries:

  1. semver – a simple CLI used to interact with semantic versions, broadly speaking it covers the following: bumping the major/minor/patch version, extracting version information, overriding version information, and comparing semantic version strings
  2. semver-git – a CLI used to find existing semantic version tags against a git repository, and generate new ones when required by incrementing the desired major/minor/patch component, with optional support for setting a prefix, build and prerelease metadata.

How does it work?

The USAGE documentation provides a good overview for how the simpler semver CLI functions, the rest of this post will discuss how the semver-git tool works.

First, let’s imagine we have a git repository with the following commits and tags:

  gitGraph
    commit id: "a1" tag: "v0.0.0"
    commit id: "a2"
    commit id: "a3"

The first commit in the main branch (gitref: a1) is tagged with the semantic version v0.0.0.

Fetching semantic version tags

The simplest thing we can do here is use semver-git to find the most recent semantic version tag searching back from the latest commit:

# defaults to using the current working directory as the repository, searching backwards from the current HEAD commit
> semver-git fetch-tag

# semver-git prints out JSON formatted information about the discovered semantic version tag
{
    "tag": "v0.0.0",
    "version": "0.0.0",
    "commit": "a1"
}

We invoked semver-git fetch-tag without any parameters which means it uses defaults:

  1. The current working directory is used as the repository we’re searching
  2. The HEAD commit is used as the starting commit

From here, the tool will search backwards through the linear history from the starting commit, looking for any commit that has a Git tag in the format of a semantic version. In our case, the tool started at commit a3 and searched backwards until it found commit a1 which had the tag v0.0.0.

We can tweak the behavior of fetch-tag through the following parameters:


FlagDescriptionDefault
--repoPath to the Git repository to be searched.
--commitGit reference for the commit to begin searching fromHEAD
--exactIf specified, only tags on the exact --commit are checkedfalse

Creating semantic version tags

The create-tag command is used to create a new semantic version tag on a specified commit. It auto-discovers the most recent semantic version by searching using the same logic as in fetch-tag above and then creates a new tag by incrementing the desired semantic version component.

The simplest invocation would be this:

# defaults to using the current working directory as the repository, searching backwards from the current HEAD commit
> semver-git create-tag

# semver-git prints out JSON formatted information about the newly created semantic version tag
{
    "tag": "v0.0.1",
    "version": "0.0.1",
    "commit": "a3"
}

And we now have a Git repository with the following commits and tags:

  gitGraph
    commit id: "a1" tag: "v0.0.0"
    commit id: "a2"
    commit id: "a3" tag: "v0.0.1"

You can see from the above that the tool did the following:

  1. Started at commit a3 and searched backwards for a semantic version
  2. Found v0.0.0 as the most recent version against commit a1
  3. Incremented the PATCH version component by 1, leading to v0.0.1
  4. Created a new tag v0.0.1 on commit a3

All of this is in line with the default behavior you get when passing no parameters to the create-tag command, we can affect the behavior of create-tag in quite a few ways:


FlagDescriptionDefault
--repoPath to the Git repository to be searched.
--commitGit reference for the commit to begin searching fromHEAD
--increment-typeWhich component of the version to increment for new tags: major, minor, or patchpatch
--annotatedIf specified, creates an annotated Git tagfalse
--prereleasePre-release identifier to set on the new semver tag.
For example: 1.2.3-alpha
none
--build-metadataBuild metadata string to set on the created semver tag
For example: 1.2.3+extended
none
--pushIf specified, the new tag will be immediately pushed to a remote repositoryfalse
--upstreamThe name of the repository upstream where the tag should be pushedorigin
--create-initial-versionIf specified, and no previous semantic tag can be found, a new one will be createdfalse
--initial-versionWhen using --create-initial-version, this flag specifies the newly created semantic (e.g., 0.0.0).none

These parameters allow us to cover the variety of use-cases we have encountered when integrating the semver-git create-tag command into our automated pipelines including:

  • Defaulting to patch version increments unless otherwise instructed
  • Automatically incrementing the major, minor, or patch version type based on GitHub labels attached to pull-requests
  • Setting build metadata based such as Hugo’s “extended” vs. “normal” mode
  • Setting prerelease data as part of CI testing flows that still require semantic versioning
  • Creating a sensible default version to make pipelines resilient when no tags exist
  • Automatically pushing created tags back to an upstream repository

One more thing: prefixes

The above examples of fetch-tag and create-tag both operate on semantic version tags on a git repository with the format vX.Y.Z, we refer to these as versions without a prefix.

There are situations where we want to manage multiple sets of semantic versions for a single repository:

  • When we are working in a monorepo
  • When we are publishing multiple docker images from a common codebase
  • When our repo is a Go workspace containing multiple Go modules

In these situations, both the fetch-tag and create-tag commands can take a parameter named --prefix. This parameter defaults to none.

  • Where --prefix is not specified, the tool will search for (and create) semantic version tags with the format vX.Y.Z
  • Where --prefix is specified (e.g., --prefix=foo), the tool will search for (and create) semantic version tags in the format foo/vX.Y.Z

Any characters that are part of a valid Git tag can be used in the --prefix value, giving you flexibility in how you structure them.

This allows us to maintain distinct sets of commits:

  gitGraph
    commit id: "a1" tag: "foo/v0.0.0" tag: "bar/v0.0.0"
    commit id: "a2" 
    commit id: "a3" tag: "foo/v0.0.1"
    commit id: "a4" 
    commit id: "a5"
    commit id: "a6" tag: "foo/v1.0.0" tag: "bar/v1.0.0"

Want to try it out?

If you’d like to try it out, pop over to the repository and see the install instructions and usage documentation

Wrapping up

Thanks for reading to the end. We hope you find semver-utils useful. If you want to find more about CECG’s approach to solving problems or how we can help you with all things Platform Engineering and Delivery, feel free to contact us .