Semantic Versioning (often referred to a SemVer) is a versioning standard that has become the defacto standard for projects to communicate the types of changes in a release.

As a programmer, I tend to be rather lazy and will do whatever I can to automate away any repetitive work.

One of those repetitive items is calculating the next incremental version and releasing it. With my recent projects garnering larger contributor bases, the number of pull requests and releases across all repositories continues to increase, and I started making mistakes in my haste to keep up (like forgetting to tag a release).

I knew there had to be a better way to do this, but I wasn’t able to find an off-the-shelf solution that didn’t rely on commit messages, and with inner-sourced codebases, it wasn’t practical to try and enforce commit messages. I needed something more intelligent and language agnostic when I stumbled upon a tool called auto.

Auto

Auto is a tool that examines the GitHub pull request history since the last tagged release and calculates the semantic version bumps based on labels. For example, if you merge a pull request labeled minor, auto will bump the semantic version a minor version. It has an extensive plugin system and a lot of other features including changelog generation.

While auto is great, it does have one incredibly annoying limitation; it only outputs patch, minor, or major instead of what the next numeric version would be.

Git Semver

And that’s where I introduce git-semver, the second half of this solution. It takes the word-based version bumping that auto generates and converts it to a numbered version based on the existing release tags.

Now, running it in a pipeline is a bit tricky since I only want the number output and nothing else, so I use the --dryrun flag so it doesn’t try to manage the release for me.

Bringing it Together

Piecing it together looks something like the following, with the SEMVER_VERSION containing the actual numeric number for the new release.

export GH_TOKEN=mysupersecretgithubtoken
export GH_ORG=mygithuborg
export GH_REPO=mygithubrepo

SEMVER_ACTION=$(auto version --repo $GH_REPO --owner $GH_ORG) SEMVER_VERSION=$(git semver $SEMVER_ACTION --dryrun)

For non-master builds, I also added development builds by taking a sterilized branch name and git commit hash.

CLEAN_BRANCH=$(echo $BRANCH_NAME | sed s/[[:punct:]]/_/g | tr "[:upper:]" "[:lower:]")
GIT_SHORT=$(git rev-parse --verify HEAD --short=8)
echo "$SEMVER_VERSION-$CLEAN_BRANCH.$GIT_SHORT"

Once I had it working, I took it a step further and baked the whole thing into a docker container which is available on DockerHub and can be called right into any pipeline. In my case, I was using Jenkins, so the integration looked something like this:

withCredentials([usernamePassword(credentialsId: '<your_github_credentials_id>', passwordVariable: 'GH_TOKEN', usernameVariable: 'GH_USER')]) {
    withEnv(['GH_ORG=<your_github_org>', 'GH_REPO=<your_github_repo>']) {
        docker.image('laiello/automatic-semver:latest').inside('-u 0') {
            checkout scm
            NEXT_VERSION = sh (
                script: 'calculate_semver',
                returnStdout: true
            ).trim()
        }
    }
}

While not the most elegant thing to look at, when paired with ghr for automating the GitHub releases, it completely revolutionized the way I manage merges to master and releases… because I don’t. Except for the code review and merge, everything else is automated.