A Single CI Task for Rails

I recently battled some serious frustration in my Rails project; I’m posting what I learned here in hopes that it helps someone in the future.

I like to have a single Rake task named ci for my continuous integration setup to use. Whether it’s a standard Ruby project, a Rails project, or even an Android project, continuous integration should be able to run rake ci and produce all of the results it needs. No chaining tasks. No chaining commands. No rain dance with environment variables. rake ci. Here’s how I did it in my Rails 3.2 app.

Here’s my ci task:

desc 'Continuous integration task'
task ci: %w[ ci:prepare file_store:clear db:migrate ] do
  exec({ 'RAILS_ENV' => nil }, 'rake', 'spec', 'spec:javascripts')

The problem stems from chaining tasks involving the database, which puts the Rails.env into the development environment, and RSpec’s tasks, which puts Rails.env into the test environment. Run either one by itself and it’s fine, but chain them, with say rake db:migrate spec, and the spec task doesn’t work properly. This is because the Rails.env is development, not test.

Specifying the command as RAILS_ENV=test rake db:migrate spec works, but again, I only want to depend on environment variables as a last resort. Likewise, specifying the command as two chained commands works— rake db:migrate && rake spec, but I want to avoid chaining as well. My preference is for a developer to type rake ci and that’s it. No forgetting what to type. No digging through history to remember the proper commands 2 years later. No emailing former developers and hoping they remember. The less typing, the better.

This guy ran into the same problem and wrote about it here ; it sounds like he was going for the same goal—a single ci task—but ran out of options and fell back on chaining commands. After I read this I almost went with the same approach, but at the last moment remembered exec.

Here’s my ci task again:

desc 'Continuous integration task'
task ci: %w[ ci:prepare file_store:clear db:migrate ] do
  exec({ 'RAILS_ENV' => nil }, 'rake', 'spec', 'spec:javascripts')

I have the ci task’s dependences setup to run all of the database migrations and what not. The guts of the task then use exec to re-exec rake, but with the RAILS_ENV cleared out. Now the spec and spec:javascripts tasks can setup the test environment the way they expect.

Now, of course, this isn’t perfect. If I’d wanted to run some code after the exec, well, that wouldn’t work. And there are some tradeoffs and preferences to be made – I could have set ENV['RAILS_ENV'] = nil instead of passing 'RAILS_ENV' => nil to exec. But I like how the environment changes are encapsulated to the exec call.

Last but not least, my preference for a single ci task is also, well, a preference. But it’s worked quite well for me over the last half dozen years. If you share this preference then I hope this use of exec is helpful.

Edit 3/26/2012: Fixed an accidental textile formatting problem.

Edit 3/27/2012: Corrected the capitalization of a proper noun.

  • Aaron Jensen says:

    I do this: https://gist.github.com/2220877

    It’s faster and prevents you from needing a development environment on your ci machine. Of course if you want one in order to test migrations that’s fine, but we test our migrations during staging deploy which is more “real”.

  • […] as a valuable practice, but getting all the pieces together can be a pain. Matt Fletcher shared some of his difficulties in getting things setup satisfactorally for Rails. After much experimentation, I’m actually […]

  • Comments are closed.