Automate All the Things!

Doug Ireton's blog about Chef, Git, Ruby, Vim, and Infrastructure Automation

Creating a Git Pre-commit Hook for Chef Cookbooks

| Comments

We’ve been using Chef in our group now for a few months, but until now we haven’t been serious about linting or testing our Chef cookbooks. I decided to get serious today and write a Git pre-commit hook for linting cookboks.

Git runs the pre-commit hook script before each commit. This allows you to run code quality checks so only clean code is committed to your repo.

It’s important to note that git hooks aren’t copied down when you clone a git repo. Each developer will need to create his or her own pre-commit hook script in the .git/hooks/ directory of the repo. If you wanted to get fancy, you could keep git hook scripts in a “utility” repo and have a rake script to copy them to the right location.

The pre-commit script below does four things:

  1. Runs a built-in Git whitespace check for trailing whitespace, mixed tabs and spaces, etc.
  2. Runs ‘knife cookbook test’ to check Ruby and ERB template syntax.
  3. Runs ‘tailor’ to check your code against Ruby style conventions.
  4. Runs , the de facto Chef cookbook linting tool.
(pre-commit) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#!/usr/bin/env ruby

# check for whitespace errors
git_ws_check = `git diff-index --check --cached HEAD --`
unless $?.success?
  puts git_ws_check
  exit 1
end

COOKBOOK_PATH = File.split `git rev-parse --show-toplevel`
PARENT_PATH = COOKBOOK_PATH[0]
COOKBOOK_NAME = COOKBOOK_PATH[1].chomp # remove trailing newline

puts 'Running knife cookbook test...'
knife_output = `knife cookbook test #{ COOKBOOK_NAME } -o #{ PARENT_PATH } -c ~/chef/wit/chef-repo/.chef/knife.rb`
unless $?.success?
  puts knife_output
  exit 1
end

puts 'Running tailor...'

# Get the file names of (A)dded, (C)opied, (M)odified Ruby files 
STAGED_FILES = `git diff-index --name-only --diff-filter=ACM HEAD -- '*.rb'`
STAGED_FILES.lines do |file|
  file.chomp! # remove carriage returns
  puts file
    tailor_output = `tailor --max-line-length 999 #{ file }`
    unless $?.success?
      puts tailor_output
      exit 1
    end
end

puts "Running foodcritic..."
fc_output = `foodcritic --epic-fail any #{ File.join(PARENT_PATH, COOKBOOK_NAME) }`
unless $?.success?
  puts fc_output
  exit 1
end

But, how do I use it?

Just copy the script below to file named ‘pre-commit’, make it executable, and copy it to the cookbooks/cookbook_name/.git/hooks/ directory.

Wait a minute! It’s not robust!

You may have noticed that the script needs a few things. It should check for the existence of various binaries (knife, foodcritic, tailor) before calling them. I’m sure you could think of many more improvents. I welcome your comments or gist forks. I just had to move on to more pressing things.

Thanks for reading and I welcome constructive comments…

Comments