2017. május 24.

GitHooks For a Safer Go Project


There is this paradigm that before every commit you should run a designated set of unit tests to be certain that your code compiles/runs/is mostly bug free. But that’s not always the case. I’m a huge advocate of automation and restrictions via coded gates. People tend to forget things and ignore guidelines, which are usually in Gods know how long text hidden somewhere on a Google Drive folder that nobody knows about other than the head of the QA department.


Rather than trying to remember things like, “Run these set of tests for this package.” or “Just run `make clean build install test supertest hardtests slowtests fasttests pluginPackage_Tests_WhichareSuperVeryIMPORTANT`”, just code up standards and gates at the appropriate location. 

Now, there are several parts of a process where there can be gated solutions. PR checks, end-to-end / integration tests, unit tests, manual tests, all sorts of tests. If, however, you are working in a large team, with 100+ developers banging on the same codebase, you will end up in a resource bottleneck eventually. 

There are, again, a couple of ways to avoid that. Maximum number of parallel checks, parallel worker servers, and optimizations here and there. What I’m about to show you is one of these small, yet noticeable optimizations. 

In my experience...

...about 20% - 30% of the commits which are checked on a CI server end up being a small error, which would have been caught, if the individual had run the appropriate test group. But this can easily be avoided by a simple GitHook. A GitHook has the benefit that it can detect which package a change occurred, and execute a small set of focused, specialized tests BEFORE allowing the push upstream. It can also be further fine tuned to disallow certain pushes. For example, direct push to master, or the creation of branches with a reserved name, such as released version branches. 

Three things are required for this to work.

First, the code

In order for this to be as effective and painless for the developer as possible, these tests need to be very fast. And by very fast, I mean, under a second or two TOPS. Fortunately, Go has us covered in this department. These tests could be identified by a distinctive postfix like ‘QuickCheck,’ for example. To run these with Go, you’d simply type: go test -v ./commands/ -run “.*QuickCheck$”. The ‘-v’ is for verbosity. And the -run accepts a regex for choosing what test to run.

Second, still the code

This is more of a convenience. It’s nice if the tests are sitting next to their source counterpart, because it will be easy to run tests only in the packages where git detects changes. 

Third, the hook of course

Edit the file under .git/hooks/pre-push.sample and add this at the end before the exit 0.

  1. dirs=$(git status -s | xargs -I {} dirname {})
  2. for d in ${dirs[@]}; do
  3.     if [ $d != '.' ] && [ $d != '..' ] && [[ ! $d =~ ^[M,D,A]$|^AD$ ]]; then
  4.     go test -v ./$d/ -run=".*Quick$"
  5.        RESULT=$?
  6.        # Fail early so we don’t waste time on more runs.
  7.        # This is up to the user to decide.
  8.        if [ $RESULT -ne 0 ]; then
  9.            echo "Failed test run in ${d}. Disallowing push."
  10.            exit 1
  11.        fi
  12.     fi
  13. done

After this, rename the file to pre-push by removing the .sample extension from it.
If you mess something up, you should see something like this before your push:


  1. # github.com/yourepo/project<br />
  2. === RUN   TestConfigPathQuick<br />
  3. Config path is:  ~/.config/project<br />
  4. --- FAIL: TestConfigPathQuick (0.00s)<br />
  5.     config_test.go:16: asdf<br />
  6. FAIL<br />
  7. exit status 1<br />
  8. FAIL    github.com/yourepo/project/config    0.012s<br />
  9. Failed test for folder 'config'.<br />
  10. error: failed to push some refs to 'git@github.com:yourrepo/project.git'


There are occasions where you would like to avoid running the hook. For example, you would like to push into your own branch, and don’t care about breaking it. For that, there is `git push --no-verify` which skips the push hook.

Closing Words

I can’t stress enough that these should be quick tests! You don’t want to sit there and see tests being run before each and every commit for minutes! You will surely end up with a bunch of angry devs banging on your door to “Get rid of that piece of…”.

Thank you for reading!


Related posts

2018. november 27.

An extensive tutorial on how to use HashiCorp's go-plugin to extend your own project.