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:
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 stringssemver-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:
- The current working directory is used as the repository we’re searching
- 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:
Flag | Description | Default |
---|---|---|
--repo | Path to the Git repository to be searched | . |
--commit | Git reference for the commit to begin searching from | HEAD |
--exact | If specified, only tags on the exact --commit are checked | false |
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:
- Started at commit
a3
and searched backwards for a semantic version - Found
v0.0.0
as the most recent version against commita1
- Incremented the PATCH version component by 1, leading to
v0.0.1
- Created a new tag
v0.0.1
on commita3
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:
Flag | Description | Default |
---|---|---|
--repo | Path to the Git repository to be searched | . |
--commit | Git reference for the commit to begin searching from | HEAD |
--increment-type | Which component of the version to increment for new tags: major , minor , or patch | patch |
--annotated | If specified, creates an annotated Git tag | false |
--prerelease | Pre-release identifier to set on the new semver tag. For example: 1.2.3-alpha | none |
--build-metadata | Build metadata string to set on the created semver tag For example: 1.2.3+extended | none |
--push | If specified, the new tag will be immediately pushed to a remote repository | false |
--upstream | The name of the repository upstream where the tag should be pushed | origin |
--create-initial-version | If specified, and no previous semantic tag can be found, a new one will be created | false |
--initial-version | When 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 formatvX.Y.Z
- Where
--prefix
is specified (e.g.,--prefix=foo
), the tool will search for (and create) semantic version tags in the formatfoo/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
.