(Picture courtesy of Sean McClean)

Using GitVersion in GitHub Actions beta

I was lucky to enter the GitHub Actions beta program so I wondered about the best way to test it. Finally, I decided to port Aggregator CLI build scripts to GitHub Actions.

A critical step of those scripts is to run GitVersion to generate the version for Aggregator. GitVersion, in the words of its authors, «looks at your git history and works out the semantic version of the commit being built.« I will spend a few more words later on the versioning topic, explaining why it is so important, but for now let’s stick to the mechanics of running this tool.

The first issue is that GitVersion is not installed on Linux GitHub agents.

GitVersion not working

So, GitVersion is not installed and there is no existing Action for it. There is still a way: GitHub Actions are mostly written in Javascript but they allow to run any Docker container which is published on a publicly-available Docker registry. Fantastic! GitVersion has a both a Windows and a Linux image in Docker Hub. Note that such Docker GitHub Action is available only on Linux agents, so the first step to use GitVersion is to require such Linux agent using the runs-on clause this way:

    runs-on: ubuntu-latest

The second trick was harder to discover: in my first attempts of running GitVersion, I found this error in the logs.

ERROR [09/12/19 19:23:21:93] An unexpected error occurred:
System.InvalidOperationException: Could not find a 'develop' or 'master' branch, neither locally nor remotely.
  at GitVersion.Configuration.BranchConfigurationCalculator.InheritBranchConfiguration(GitVersionContext context, Branch targetBranch, BranchConfig branchConfiguration, IList`1 excludedInheritBranches)

What? Yes, I was working on a development branch, but why GitVersion cannot find master? I never saw this problem with other pipelines.

Well, in the log of the checkout action, after pulling the code, you see two git commands. The last command, git reset --hard <sha-commit-matching-triggering-event>, is pretty common and put the local repository in detached HEAD, so this is not causing GitVersion error.

Going backward in the log, I found a line with this command git checkout --progress --force -B refs/heads/<branch> refs/remotes/origin/<branch>. Aha! This checkout materialise only the branch which triggered the event and no other.

This has a simple solution: run an explicit git branch to guarantee that a local master is available to GitVersion.

I also found that the initial git pull in the checkout action has an explicit --no-tags option. Tags are necessary to GitVersion, otherwise it may compute an incorrect version, and we need to fix this too.

In summary, I added a step in the workflow (that is the name for a build script in GitHub Actions), just after the checkout action, to put the git repository status the way GitVersion needs.

    - uses: actions/checkout@v1
    - run: |
        git fetch --tags
        git branch --create-reflog master origin/master

With this correction the GitVersion error stops showing up.

More GitVersion tricks

I have another couple of useful tips.

First is the position of the UpdateAssemblyInfo option: it must be the first option or the AssemblyInfo files are left untouched. Note also the use of a dash - instead of a slash / character to introduce an option. This is required when running GitVersion on Linux. Here is a working example:

    - name: Update AssemblyInfo files
      uses: docker://gittools/gitversion:5.0.2-linux
      with:
        args: '-updateassemblyinfo -nocache -output buildserver'

You may wonder how the Docker container can alter the repository which is on host filesystem. Well, there is no magic: the GitHub Docker action automatically mounts the root of the repo as docker volume in a fixed directory, /github/workspace; it also sets /github/workspace as the current directory for the container. The dockerised tool acts on the current folder without realising it is touching the host filesystem.

The second tip is a technique to edit other files in addition to AssemblyInfo files. As GitVersion runs in a container, it is non-trivial to export the environment variables it computes. I found easier to edit the files from within the container using the exec option. This option runs a nested command inheriting all environment variables just computed by GitVersion.

    - name: 'Set version in aggregator-manifest.ini'
      uses: docker://gittools/gitversion:5.0.2-linux
      with:
        args: '-nocache -output buildserver -exec /bin/bash -execargs "-c \"sed -E -i \"s/version=.*/version=$GitVersion_FullSemVer/\" /github/workspace/src/aggregator-function/aggregator-manifest.ini\""'

In this example, I simply run sed to edit the target file using the value of $GitVersion_FullSemVer environment variable, kindly supplied by GitVersion, as the replacement value.

Why you should use GitVersion or a similar tool

If you do not know in details what is Semantic Versioning (SemVer) I recommend following the link to the site and study the definitions.

Is SemVer a perfect and fully automatable solution? I do not think so: much is left to human judgement. On the other hand is extremely good for tools and as a message from developers to users. A great discussion on pros and cons of SemVer can be found at https://gist.github.com/jashkenas/cbd2b088e20279ae2c8e.

In my opinion, you should use SemVer whenever reasonably feasible and GitVersion is a fabulous tool that makes very easy following SemVer principles. It analyses the Git repo history looking for branches and tags and, from this information, it automatically determines the semantic version applying the rules you like.

Does it reads your mind? No, you still need to apply tags or follow a pattern in your branches, but that is all you need. You still have to use your judgement and follow SemVer rules.

Even more important is that GitVersion (or any similar tool) prevents trivial mistakes, like forgetting to increase the build version, and decrease the effort of the maintainer. It will always suggest a unique set of identifiers for our artefacts and this is essential for implementing traceability. I preached about traceability for decades, now. Even with Continuous Deployment it is easy to lose track of which code landed on users’ hands without basic techniques like the ones supported by GitVersion.

That’s all for today and, as always, happy building!