Lessons in using rake for builds and deploys

4 minute read

I've been using rake for a few years now to do builds and deployments. It first started off at Huddle, as a way of escaping NAnt due to it's xml nastiness and lack of power. Being honest, it was a naive step into Ruby and the outcome was a poorly written, C# styled set of scripts that were untested. From that point I've learnt a number of lessons...

Best Practise

If you're new to Ruby, first things first, find out how write and use the language as per it's typical standards. This goes from naming classes, to methods, to requiring other classes. I've decided not to include any links as this should be an exploratory phase. Read a book, go on Google and start searching for terms such as "Ruby best practise" etc..., find some well-known, mature Ruby projects in Github and have a browse around the code. There are plenty of places to find where Ruby is written correctly.

You're doing something wrong if:
  • You find you're self doing more than a simple "require 'myclass'" and have __FILE__ expands etc... for referencing other classes.
  • Your scripts are littered with GLOBAL_CONSTANTS that are being used by child "required" Ruby files.
  • Your classes aren't as clean as they would be in another language. Say for example, you have come from C#. An equivalent Ruby class should look just as clean, if not a little cleaner.
  • Your rakefile is not easy to read, and see what tasks it can perform, ie tasks are created and hidden in the depths of your code
  • Your putting complex logic in without any tests around it.

Keep It Simple

I've written my own build framework, rakeoff, but generally speaking it's not typically required as it will add complexity and logic behind the scenes that others won't know about. Your scripts should be fairly simple as building, testing and deploying often aren't complicated steps.

Where possible, do the simplest thing possible, ie just get rake to execute a command line. If the command line fails, you'll see what and why in the build logs. You don't need anything more clever than that. By keeping it closer to command lines means it's easier to understand and replay locally.

 task :msbuild do  
sh '%windir%/microsoft.net/framework/v3.5/msbuild.exe /t:Clean;Build /p:Configuration=Release /m /consoleloggerparameters:ErrorsOnly MySolution.sln'
end

Gems & Bundler

Using gems (and Bundler) should be pretty standard. Not only for external libraries, but internal ones. Don't go writing a bunch of scripts that are shared across applications, because when you update them you risk breaking all of your apps. Allow apps to determine what version of your scripts they want to run via a Gemfile.

Also it's far nicer to build a gem, then simply have a "require 'blah'" in your code and it's working, rather than trying to include a Ruby file (via a submodule if it's shared).

Visibility

If you're not going to use "rake --tasks" for seeing what tasks are available (and from my experience not many people do that), then I would advise keeping all your tasks inside your main rakefile. Ie, don't have tasks hidden away in other scripts, keep them visible upfront in your rakefile.

 task :unit_tests do  
run_tests('*.Unit.Tests')
end

task :integration_tests, :environment do |t, args|
run_tests('*.Tests.Integration', args.environment)
end

task :acceptance_tests, :environment do |t, args|
run_tests('*.Tests.Acceptance', args.environment)
end

task :smoke_tests, :environment do |t, args|
run_tests('*.Tests.Smoke', args.environment)
end

def run_tests(pattern, environment='local')
// Do some stuff
end

Testing

In cases where you're simply calling a command line, you shouldn't really need testing, assuming the command line will fail if mis-used (if it won't they you may need to consider tests). Your build environment will act as your tests and show you when something is broken. This should not affect live, as your internal environments should deploy in the same way, so you'll find out if something is broken internally first.

If on occasion you do need to test some logic, then create a class that can be easily tested (as rake tasks can be a pain to test). Your rake task then just needs to call it (keep the task as thin as possible).

 task :unit_tests do  
NUnit.new.run('*.Unit.Tests')
end

Artifacts

One area you can easily break your build is around building up artifacts. If this dependency is put in your build environment (such as Team City), then it won't be testable. Often builds will fail if they can't find an artifact (say you want to deploy a build), but they may not if the artifacts exist, but are missing files, or aren't correct for whatever reason. Because of this I would either advise:

a) keeping artifact configuration as simple as possible in your build environment. If you've got more than a few lines for including artifacts, then you should question if building the artifacts is more complex than it needs to be.

b) building your artifacts in your Ruby scripts. That way you can, if needs be, test them and replicate it locally. Being able to replicate an artifact build locally is very useful when trying to fix an issue.

Updated: