git2semver

Git2semver is a small NPM module which generates a version number based on the contents of the git commit log. I've released the code as open source and you can try it out in your own repository by issuing the following command.

$ npx -q git2semver
1.5.7

NOTE: The module is called git2semver, the npx command is been around since 2017 and allows you to download a module on the fly which can be very useful for these little Node.js based utilities. If you want to lock to a specific version you could do the following:

$ npx -q git2semver@0.4.0
1.5.7

How it works

Git2semver works by spawning the git command under the covers and processing its output. It looks for the most recent tag that matches the semver format and then uses that as a baseline as it walks back to the top of the commit log looking for messages with (by default) major:, minor: and patch: prefixes. As it encounters these messages it increments the version number accordingly.

If you invoke the git log command you end up with some output that looks like this:

$ git log --format=oneline
431f598b7a93c67310b3814ac524836fd17236a7 (HEAD -> master) minor: added non-breaking command-line option.
bc762c8420b9f283c24cfa1f0aa9c7e59df3fd99 major: breaking API change.
1bef93bf0deeef0b96d6578edee8c94a862ea3e5 Random change, no suffix.
08ae52e21bc8ce0e5d76b0b723bd08c0d8510002 patch: added missing file
fe309c554df5d103c52176c4092a0eff05b4c03f (tag: 0.1.0) Initial commit.

Git2semver would reason over this log in the following way:

  1. First, it would find commit starting with hash fe309c55. Because this commit has a tag with a valid version string (0.1.0) it uses that as the base line.
  2. It then reads commit starting with hash 08ae52e2 and because it has the patch: prefix it increments the patch number resulting in a version of 0.1.1.
  3. There are more commits so it keeps reading until it gets to commit starting with hash bc762c84 which starts with the major: prefix which increases the version number to 1.0.0.
  4. There is one more commit with a minor: prefix (hash starting 431f598b) which results in a final version number of 1.1.0.

The output is kept deliberately minimal so that the output can be piped directly into a variable in a script for later use (also adheres to concept of DOTADIW or "Do One Thing and Do It Well).

$ export VERSION=`npx -q git2semver`
$ echo $VERSION
1.5.7

Under the covers git2semver is using the excellent semver module parse and increment version numbers. Even though by default I just output the final version number you can get at the full object that the semver module uses to represent a version number and all its options and components. Simply invoke the following command:

$ npx -q git2semver --formatter raw
SemVer {
  options: { loose: false, includePrerelease: false },
  loose: false,
  raw: '1.1.0',
  major: 1,
  minor: 1,
  patch: 0,
  prerelease: [],
  build: [],
  version: '1.1.0' }

There are some other command-line options as well:

$ npx -q git2semver --help
Usage: git2semver [options]

Options:
  -V, --version                output the version number
  -f, --formatter <formatter>  Use formatter
  -r, --repository <path>      Path to local repository
  -c, --configuration <path>   Path to configuration object
  -h, --help                   output usage information

The repository option allows you to specify a path to the local clone of a git repository so that you don't need to change your current working directory. The configuration option requires a bit more explanation.

Configuration options

Git2semver's default options should serve you fairly well, but in cases where you want to have more control about when a version number is incremented you can create a configuration file which helps control the policy that git2semver uses to increment version numbers and format its output.

The configuration file is in the form of a loadable module (called git2semver.config.js in the root of the repository) that defines a function as its export which is then invoked at run-time to configure the policy, here is an example of a configuration file which matches the defaults:

module.exports = (policy) => {
    policy.useMainline('major:', 'minor:', 'patch:');
    policy.useFormatter("default");
};

The useMainline reference is just a shortcut that allows you to provide three prefix strings for incrementing each version segment. A more expanded version would be:

module.exports = (policy) => {
	policy.incrementMajorWhen((commit) => commit.message.toLowerCase().startsWith("major:"));
   	policy.incrementMinorWhen((commit) => commit.message.toLowerCase().startsWith("minor:"));
	policy.incrementPatchWhen((commit) => commit.message.toLowerCase().startsWith("patch:"));
    policy.useFormatter("default");
};

The incrementMajorWhen function and its peers allows you to register a callback that is invoked every time git2semver steps through a commit on the git commit log. If the function returns true then git2semver will take care of incrementing the appropriate segment of the version string. You can make this logic as complex as you like but remember that these functions are executed one every single commit (since the last tag that looks like a version string).

Strictly speaking the useFormatter function above is optional, by default git2semver will just output the final version string (x.y.z) but you can also pass in a function which takes the SemVer object shown above and allows you to format the output any way that you like.

I use Azure Pipelines for continuous integration and so I've added a custom formatter that looks like the following:

module.exports = (policy) => {
    policy.useMainline('major:', 'minor:', 'patch:');
    policy.useFormatter((result) => `##vso[build.updatebuildnumber]${result.version}`);
};

When Azure Pipelines sees this output in the build logs it automatically sets the label of the build to whatever the version number is. The azure-pipelines.yml file for git2semver is actually fairly simple:

pool:
  vmImage: 'Ubuntu 16.04'

steps:
- task: NodeTool@0
  inputs:
    versionSpec: '10.x'
  displayName: 'Install Node.js'

- script: |
    set -e
    npm ci
    npm test
    npm link
    git2semver
- script: |
    npm version $BUILD_BUILDNUMBER --no-git-tag-version
  displayName: 'npm ci, test and pack'
- task: PublishPipelineArtifact@0
  inputs:
    artifactName: drop
    targetPath: $(Build.SourcesDirectory)

In this case I restore packages, test and then link to the current version of the module and then run the git2semver command (as opposed to running npx). Notice the following script step:

- script: |
    npm version $BUILD_BUILDNUMBER --no-git-tag-version

The $BUILD_BUILDNUMBER variable gets set via Azure Pipelines after it processes the output from the preceeding script block that runs git2semver. Its important to use the --not-git-tag-version option to stop the npm version command modifying the git repository - we just use it here to easily update the version string in the package.json file. Even though I am applying a version string to an NPM package here, similar techniques apply to other packaging tools also.

If you want to use git2semver in your own Azure Pipelines you would do something like this:

pool:
  vmImage: 'Ubuntu 16.04'

steps:
- task: NodeTool@0
  inputs:
    versionSpec: '10.x'
  displayName: 'Install Node.js'
- script: |
    npx -q git2semver --formatter majorminorpatch-pipelines-variables-and-label
- script: |
    echo $BUILD_BUILDNUMBER
	echo $GIT2SEMVER_MAJOR
	echo $GIT2SEMVER_MINOR
	echo $GIT2SEMVER_PATCH

This will trigger the download of the latest version of git2semver and tell it to generate the version string based on the current directory which will the git repository that you are currently building. The formatter argument specifies a built-in formatter which outputs a specially formatted string which tells Azure Pipelines to update the build label and set a bunch of environment variables.

Motivation

I believe that the version of any artifact produced from a CI process should be versioned and that the state of the version control repository being the primary input into the build process should help to define what the version number of the artifact is. Using the git commit log as an input to producing that version number makes sense. However you can't simply hard code the version numbers into the commit log itself because it wouldn't cope with branching scenarios.

Instead - git2semver allows the developer to declare the scope of their change (by default, major, minor and patch) and then use those to derive a version number based on the state of the branch being analyzed.

There are other tools out there that do the similar things such a semantic-release and GitVersion. Each tool has its pros and cons - semantic-release provides more of an end-to-end workflow for publishing packages to the public NPM registry and GitVersion provides a very well thought set of rules for generating semantic version numbers which understands branching strategies.

Historically I've used GitVersion with my Azure Pipelines builds but the support on hosted Linux is a bit spotty due to some libgit2 compatibility issues - hopefully that get resolved soon - in the meantime I've got git2semver (which I'm sharing with you). I did also consider using semantic-release but it would have taken longer for me to integrate it with Azure Pipelines and Azure Artifacts and it is more focused on NPM only scenarios whereas git2semver's narrow scope makes it easier to adopt.

Anyway - git2semver exists, it is a small low-dependency (semver, commander) module that you can easily integrate into your build process to derive a semantic version number from your git commit log.

Try it out

If you are interested in trying out git2semver there is no real setup required to see how it will work against you repository - just use the following command and see what it produces:

$ npx -q git2semver
1.5.6

If you experience any problems don't forget to log an issue (or submit a PR)!