Avoid Lint Errors in CI with Git Hooks

Static analysis tools, such as linters, are very useful for complex software projects, especially when working in JavaScript, Ruby, or any other dynamic language. The linter will parse your code and search for anything suspicious, while enforcing a certain (configurable) set of stylistic rules.

There’s Lint in my CI

It is a common practice to run your linter in your CI test suite, to ensure that your code adheres to the lint before it is released. However, actually running the linter requires a manual step that often goes overlooked. What happens is that you push a commit, only to have CI tell you that your lint failed. Now, you have to go back and fix the lint errors, and then push it again.

Ideally, you can rebase the commit to avoid a bunch of “linting…” commit messages strewn throughout your code, but this means more work to clean up the Git history. Overall, this whole linting issue has been a long-time annoyance for me, but until recently, I had never thought about a better solution.

Git Hooks to the Rescue

A few months ago, as I was starting on my current project, I took a stab at solving this problem. I just want my code to stay linted, and to make sure that the linter is run before I push out any code in Git. I thought to myself, “If only there were hooks for Git…” Then I remembered that there are–and logically, they’re called Git hooks.

Git hooks are just scripts which Git will execute before or after various lifecycle events. For example, pre-commit, pre-rebase, post-checkout, pre-push, etc. In particular, the pre-push was the one that interested me. Before the git push command succeeds, I want to check if I changed any applicable files. If I did, I want to run the linter and bail if the linter fails, preventing code from leaving my machine with failing lint.

The Code

Below is the simple Bash script to perform the lint check. Just place this in .git/hooks/pre-push, and you’re all set!


#!/bin/bash

set -e

remote="$1"
url="$2"
z40=0000000000000000000000000000000000000000

while read local_ref local_sha remote_ref remote_sha
do
  if [ "$local_sha" = $z40 ]; then
    # branch deleted
    :
  else
    if [ "$remote_sha" = $z40 ]; then
      jshint=1
      scsslint=1
    else
      range="$remote_sha..$local_sha"
      files=`git diff --name-only $range`

      [ -n "`echo $files | grep -E '\.js$|\.jsx|\.ts|\.tsx$'`" ] && jshint=1
      [ -n "`echo $files | grep \.scss$`" ] && scsslint=1
      # if you have other file types to lint, regex for them here
    fi

    if [[ $jshint -eq 1 ]]; then
      # run your lint script for js/ts here
      yarn lint-ts --target=production -c ./tslint-git-push.json
    fi

    if [[ $scsslint -eq 1 ]]; then
      yarn lint-css --target=production
    fi
  fi
done

exit 0

This is definitely a brute-force solution, but it does the trick pretty nicely. Nothing is worse than failing a build on CI, only to discover that it was because you used double quotes instead of single quotes.

I should also note that using an editor/IDE with lint support (ESLint, TSLint, RuboCop, etc.) is also an effective way to eliminate lint errors in the first place. However, even with proper IDE support, it’s still nice to know that Git has your back and will prevent you from pushing un-linted code to CI.

I hope you found this helpful, but I am also curious what tools you have used to help with this issue. I’d appreciate some feedback!