Channels ▼
RSS

Design

Driving Continuous Integration from Git


Think Globally, Hook Locally

We know that the sooner an issue is discovered, the easier (and faster and cheaper) it is to fix. That's why hooks that operate on local clones of a repository are so useful: They offer immediate feedback. Because we don't get the cmd prompt back until a hook completes, client-side hooks should be limited to operations that take only a few seconds, lest the development flow be interrupted. Let's look at two hooks that complete almost instantly.

Get Branch Build Status

Exposing branch build status in the terminal window with a post-checkout hook catches two fish with one worm: It provides actionable information, and eliminates the need to switch applications to get it. Upon checkout (and remember, in Git "checkout" means switching branches, not pulling down code as with SVN and Perforce), this hook grabs the branch's head revision number from the local copy. It then queries the CI server to see whether that revision has been built, and if so, whether the build succeeded.

#!/usr/bin/env ruby
 
 # post-checkout hook for determining the build status of the 
 # checked out ref from the CI server.
 #
 # Requires Ruby 1.9.3+ 
 
 require 'yaml'
 require 'json'
 require 'net/https'
 require 'uri'
 
 # utility for correctly pluralizing quantities
 def pluralize count, single, multiple
   count == 1 ? single : multiple
 end
 
 # parse args supplied by git
 ref = ARGV[1]       # ref being checked out
 isBranch = ARGV[2]  # 0 = file checkout, 1 = branch checkout
 
 # we only care about branch checkouts 
 if isBranch == "1"
   # initialise build status counts
   failed = successful = in_progress = 0
   
   # loop through each configured Stash server, retrieving build 
   # statuses for the checked out commit and 
   # counting the number of failed, successful and in progress builds
   hookDir = File.expand_path File.dirname(__FILE__)
   configPath = hookDir + "/bamboo-config.yml"
   raise "No bamboo-config.yml found." unless File.exists? configPath
   config = YAML.load_file(configPath)
   raise "bamboo-config.yml file is incomplete: 
       username, password & url are required" unless 
       config['url'] and config['username'] and config['password']
     
   # normalize base url
   baseUrl = config['url']
   # assume https if no scheme spcified
   if not baseUrl.start_with? "http"
     baseUrl = "https://#{baseUrl}"
   end
   # strip trailing slashes
   while baseUrl.end_with? "/"
     baseUrl = baseUrl[0..-2]
   end
 
   # prepare a request to hit the build status REST end-point
   build_status_resource = 
       "#{baseUrl}/rest/api/latest/result/byChangeset"
   uri = URI.parse("#{build_status_resource}/#{ref}")
   req = Net::HTTP::Get.new(uri.to_s, initheader = 
       {'Content-Type' => 'application/json', 
           'Accept' => 'application/json'})
   req.basic_auth config['username'], config['password']
   http = Net::HTTP.new(uri.host, uri.port)
   http.verify_mode = OpenSSL::SSL::VERIFY_NONE
   http.use_ssl = uri.scheme.eql?("https")
 
   # execute the request
   response = http.start {|http| http.request(req)}
       
   if not response.is_a? Net::HTTPOK
     puts 'An unknown error occurred while querying 
         Bamboo for build results.'    
     exit    
   else  
     # if the request succeeded, count 
     # the statuses from the response
     body = JSON.parse(response.body)        
     body['results']['result'].collect { |result|
       case result['state']
       when "Failed" 
           failed += 1
       when "Successful"
           successful += 1
       when "Unknown"
           if result['lifeCycleState'] == "InProgress"
             in_progress += 1
           end
       end
     }
   end    
 
   # display a short message describing the build status 
   # for the checked out commit
   shortRef = ref[0..7]
   if failed > 0
     puts "Warning! #{shortRef} has #{failed} 
          red #{pluralize(failed, 'build', 'builds')} 
          (plus #{successful} green and #{in_progress} 
          in progress).\nDetails: #{uri}"
   elsif successful == 0
       puts "#{shortRef} hasn't built yet."
   else
        puts "#{shortRef} has #{successful} green 
            #{pluralize(successful, 'build', 'builds')}."
   end
 
 end

If, for example, the hook tells you the head commit on the master has built successfully, then it's a "safe" commit to create a feature branch from. Or let's say the hook says the build for that revision failed, yet the team's wallboard shows a green build for that branch (or vice versa). That means the local copy is out-of-date. Whether to pull down the updates is determined on a case-by-case basis.

This hook and its config files can be found at https://bitbucket.org/tpettersen/post-checkout-build-status.

Sanity-Check Code Style

Checking for violations at merge time is great, but a pre-commit hook analyzing the changeset keeps the style police off your back entirely. Start by capturing the names of files being updated or added and concatenating them. That string of file names is then passed into the Checkstyle run command. If violations are found, the commit is rejected.

Note that despite variations between them, all static analysis tools can be used with this approach. Findbugs, for example, must be run on the entire project because it looks at methods referenced across classes. But that's not necessarily a deal-breaker. Small and medium-sized projects can be fully analyzed quickly, especially if a generous heap space is allocated to the process.

Come As You Are

All the ideas presented here are vendor-neutral. Git hooks may not revolutionize software development the way continuous integration has, but every time a task, practice or rule is automated, it's a win.


Tim is a developer at Atlassian, while Sarah is a former test automator and scrum master who now works in product marketing.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video