Continuous Integration voor Ruby met Jenkins

29 March 2012 21:40 Iain Hecker Algemeen

Het gebruik van een Continuous Integration (CI) server is niet zo bijzonder. Traditioneel wordt een CI-server gebruikt om de code op een centrale manier te compileren. Dit verhielp het probleem dat het compileren vaak niet zo eenvoudig was. Door het te automatiseren wordt de zorg van compileren weggehaald bij de developers. De developers hoeven dan alleen de code in te checken en werkende, gecompileerde, code kwam er uit.

Tegenwoordig wordt de CI-server voor meer dan alleen compileren gebruikt. De belangrijkste taak is het draaien van de tests. Zeker bij Ruby projecten, want daar valt niks te compileren.

Wij gebruiken de CI-server Jenkins voor het draaien van onze tests en met toenemende mate ook voor het deployen van onze applicatie. Omdat de inrichting voor Ruby projecten afwijkt dan de gebruikelijke inrichting voor Java projecten, zal ik onze configuratie eens langs lopen.

Waarom Jenkins?

Jenkins (voorheen Hudson) is een CI-server met een focus op Java projecten. Waarom zou je dat gebruiken voor Ruby projecten?

Voor Ruby projecten is er ook Travis. Dit is een veel prettiger product dan Jenkins, maar draait momenteel alleen nog voor open source projecten. Omdat we bij Finalist ook Java projecten doen, is Jenkins een goede keus. We gebruiken echter relatief weinig van de standaard functionaliteit in Jenkins. Daarom hebben we sinds kort een eigen Jenkins server voor Ruby projecten. Dit is hoe we het hebben ingericht.

Github integratie

We gebruiken de Github plugin en de Github OAuth plugin om Jenkins met Github te laten praten.

Met de Github OAuth plugin zorg je ervoor dat de rechten gelijkgetrokken worden met de rechten op je Github company.

Met de Github plugin kan je automatisch post receive hooks laten toevoegen aan je projecten, zodat Github zelf de build triggert zodra er nieuwe code gepusht is.

Helaas bouwt Jenkins nog niet automatisch de juiste branch. Ook zou het handig zijn als pull requests automatisch getest zouden worden en de build status als comment toegevoegd zou worden. Maar daar kunnen we voorlopig alleen maar van dromen.

RVM

Omdat we al een tijdje Ruby doen, hebben we nog een paar applicaties die op een oude Ruby versie draaien. We proberen ze wel te upgraden naar de nieuwste versie, maar het lukt niet altijd om alles up to date te houden. Vooral door de grote verschillen tussen Ruby 1.8 en 1.9. Daarom moet Jenkins overweg kunnen met meerdere Ruby versies en RVM is daar een uitstekende oplossing voor.

Het is wel wat lastig om RVM goed aan de praat te krijgen in combinatie met Jenkins. Dit is wat wij gedaan hebben.

Eerst moeten we de rvm-shell aan de praat krijgen. De makkelijkste oplossing is door een symlink te maken in /usr/local/bin.

ln -s /var/lib/jenkins/.rvm/bin/rvm-shell /usr/local/bin/rvm-shell

Vervolgens hebben we een eigen shell geschreven die alles aan elkaar knoopt:

#!/usr/bin/env rvm-shell
rvm_project_rvmrc=1
source "$HOME/.rvm/scripts/rvm"
cd /
cd -

echo "---------------------------"
rvm info
echo "---------------------------"

exec bash "$@"

Deze serie hacks (ik heb er geen beter woord voor) doen het volgende:

  • Sta toe dat elk project zijn eigen ruby versie gebruikt.
  • Laad RVM.
  • Ga de huidige directory uit en in (dit zorgt ervoor dat RVM weer geladen wordt).
  • Toon de informatie over RVM, zoals de Ruby versie en load-paths, zodat deze in de output van de build komen.

Elk Ruby project heeft een .rvmrc file in versiebeheer, zodat elk project automatisch in de juiste Ruby versie draait. Daarnaast zorgt het ervoor dat gems van het ene project niet beschikbaar zijn voor het andere project. Een dergelijke .rvmrc ziet er als volgt uit:

rvm 1.9.3@projectnaam

Vervolgens kunnen we in Jenkins opgeven om RVM te gebruiken bij het draaien van onze tests:

Dit draait ons build script en deployt, d.m.v. Capistrano, naar onze demo omgeving. We zijn nog niet dapper genoeg om gelijk naar productie te deployen, maar dat staat zeker op de planning.

Ons motto is dat er zo min mogelijk in Jenkins komt te staan. Het build script hoort bij de code in versiebeheer! Ook alle deployment instellingen en scripts staan volledig in versiebeheer en moet met één simpel commando uitgevoerd kunnen worden. Ons vak is immers automatiseren.

Build script

Wat Jenkins precies moet doen staat bij ons altijd in een bash bestand genaamd script/ci. Dit ziet er meestal zo uit:

#!/usr/bin/env bash
set -e

export RAILS_ENV=test

run() {
  echo "=== Running: $* ==="
  time bundle exec $*
}

gem install bundler --no-ri --no-rdoc
bundle install

run rake db:create db:migrate:reset
run rspec --format progress
run cucumber --format progress --no-color

Het is geen rocket science. Dit is wat we doen:

  • Installeer Bundler.
  • Laat Bundler de gems die nodig zijn installeren
  • Maak de database (als deze nog niet bestaat), en draai alle migraties vanaf het begin (want we willen graag verifiëren of alle migraties blijven werken).
  • Draai de RSpec unit tests.
  • Draai de integratietests van Cucumber.

De run functie zorgt voor wat extra informatie en verzekert ons dat we de juiste versies van onze gems gebruiken.

Het is erg fijn om het build script als normaal script in source control te hebben staan. Dit is ook voor onszelf het startpunt als we de applicatie draaiend willen krijgen op onze eigen machine. Na het uitchecken van de code hoeven we alleen dit script aan te roepen. Alle dependencies worden geïnstalleerd en dat wordt door de tests direct geverifiëerd. Vooral als de setup wat complexer wordt, door bijvoorbeeld extra diensten zoals zoekmachines en achtergrond processen, is dit een uitkomst.

Selenium

Met Capybara is het super eenvoudig om integratietests te schrijven die Selenium gebruiken om via een echte browser de applicatie te benaderen.

Het enige wat je hoeft te doen is de driver te veranderen:

Capybara.current_driver = :selenium

De overgang van Rack::Test naar Selenium gaat meestal vlekkeloos. Als je Cucumber gebruikt, dan zorgt de @javascript-tag er ook nog eens voor dat de database-transacties, die normaliter om elke test heen zitten, nu wel gecommit worden zodat de browser er bij kan.

Om Selenium aan de praat te krijgen, moet je de “X Virtual Frame Buffer” en Firefox installeren op Jenkins. Jenkins kent de Xvfb plugin om Xvfb automatisch aan te zetten aan het begin van de build en weer af te sluiten aan het eind van de build.

Campfire notificatie

Als een build stuk gaat, willen we dat natuurlijk zo snel mogelijk weten. Daarom laten we Jenkins de resultaten op Campfire zetten.

Dit is, naar mijn mening, prettiger dan e-mail. Omdat we meerdere diensten met Campfire verbinden, kan je alle berichten direct naast elkaar zien. Als iemand eindelijk een falende build heeft weten te slagen, dan is een /play greatjob een mooie beloning.

De Campfire plugin voor Jenkins heeft helaas de beperking dat er maar 1 room uitgekozen kan worden voor alle berichten.

Conclusie

We zijn redelijk tevreden met deze opstelling. De Github integratie laat hier en daar nog wat te wensen over en Jenkins zelf is niet altijd even vriendelijk in gebruik, maar er valt prima mee te leven. Tenminste, tot dat Travis CI met een versie komt waarin ook privé projecten gedraaid kunnen worden!

Be Sociable, Share!

Reageer


een × = 4

RSS feed for comments on this post · TrackBack URI