Cruise Notes - A readers guide to Cruise Control

26th October 2008 Final Draft

From http://cruisecontrolrb.thoughtworks.com:

CruiseControl.rb is a continuous integration tool. Its basic purpose in life is to alert members of a software project when one of them checks something into source control that breaks the build.

Cruise control consists of a set of scripts for adding new projects and building existing ones, and a Dashboard application to display the results of builds

This guide describes cruise control 1.3.0

Dashboard Rails App

The Dashboard is a very simple Rails application, it has skinny controllers and well-factored views, and makes use of rails ajax capabilties, rails caching, builder and erb.

As well as controllers for Projects and Builds, cruise control also serves its own documentation from the webapp.

DocumentationController

The DocumentationController is a simple static controller it performs basic routing and caching

Documentation Routes : routes.rb

Documentation Controller : documentation_controller.rb

ProjectsController

ActionPathModel actionViews
index/projects/Projects.load_all
  • html
  • js
  • rss
  • cctray
show/projects/:idProjects.find(:id)
  • html => redirect /builds/:project_id
  • rss => projects/show_rss
code/projects/:id/code/:pathProjects.find(:id)
  • html => redirect /builds/:project_id
build/projects/:id/build@project.request_build
  • js => redirect /builds/:project_id

Projects Routes : routes.rb

BuildsController

ActionPathModel actionViews
show/builds/:project@project.last_build
  • html
show/builds/:project/:build@project.find_build(:build)
  • html
drop_down/builds/older/:projectProjects.find(:id)
  • html => drop_down.rhtml
artifactbuilds/:project/:build/*path@project.find_build(:build)
  • file => [build artifact]

Builds Routes : routes.rb

The artifact action breaks the skinny controller rule, it may well be better to refactor this into a method on the builds model eg. Build::get_artifact(path) however this is complicated by the desire to handle directories differently.

Fat artifact method : builds_controller.rb

Views

The views are very straightforward idomatic rails views.

Partials used for modularisation : templates/default.rhtml

Helpers used for cleaner erb : projects/_project.rhtml

Asynchronous requests via form remote tag : projects/_project.rhtml

RJS Response : projects/index_js.rjs

Builder used for xml output : projects/index_cctray.rxml

Scripts and Daemon

The cruise command is a simple shell script that delegates to one of the server, add_project or builder scripts depending on the command line arguments.

cruise executable script : cruise

add_project script : scripts/add_project

builder script : scripts/builder

The Model

The meat and gravy of the application is in the model (as it should be). There are three domain objects represented in the model - Project, Build and SCM (source control manager). The Project has many responsibilities including configuration, persistence and the build system. The project class delegates many of these reponsibilities to helper classes including BuilderStarter, BuilderStatus, PollingScheduler, ProjectConfigTracker, and BuildSerializer. Even with this refactoring the Project class is still over 500+ lines, further refactoring using delegates or mixins would definitely help the code readability.

Associations

Cruise control doesn't use Activerecord for persistence, instead it uses the filesystem. A project is stored as a directory in the [cruise data]/projects directory, the project has a configuration file, a working copy of the source code, and a directory for each of the builds.

There are finder methods for Projects and Builds- these query the filesystem in order to locate the desired resource.

Projects
find
Project
build builds create_build find_build last_build last_builds last_complete_build last_complete_build_status last_five_builds next_build previous_build

Filesystem used to store associations : project.rb

Initialization

Projects
load_all load_project
Project
configure read instantiate_plugins load_and_remember load_config load_timestamp

On initialization the project loads the central configuration file and the local configuration file. The configuration files are written in ruby and are expected to use Project.configure to set configuration attributes on the project. There is a clever trick here in setting a class variable to the current project on initialisation, this provides a means of accessing the current project in the configuration file

Initialisation method stores project reference : project.rb

Example project configuration file : cruise_config.rb

The Build System

Project
build_command build_command= build_if_necessary build_if_requested build_necessary? build_requested? build_requested_flag_file build_without_serialization builder_error_message builder_state_and_activity error_message log_changeset request_build save_timestamp scheduler scheduler=
Build
run
BuilderStarter
begin_builder path_to_cruise run_builders_at_startup= start_builders
PollingScheduler
run
BuildSerializer
serialize serialize timeout wait

The controller can start a build by calling request_build on a project. This method delegates starting the build to the BuilderStarter class. The build is started asynchronously by creating a new process which executes the builder script. The build request is stored in a flag file so that the build process is aware of the request.

Asynchronous build request : build_starter.rb

The builder script runs the project's scheduler loop, which calls build_if_necessary or build_if_requested to start a build. The build is started with the build method, which in turn delegates to build_without_serialization (serialization is performed by a BuildSerializer if required).

Scheduler Loop : polling_scheduler.rb

A Build object is created which controls the build and provides access to and persistence for results. The run method creates the appropriate environment and runs the project build command or rake task.

Build's run method : build.rb

Triggers

Project
build_necessary? triggered_by triggered_by=
ChangeInSourceControlTrigger
build_necessary?
SuccessfulBuildTrigger
build_necessary?

build_necessary? delegates to the projects triggers. Triggers are simple objects which respond to build_necessary?. Cruise control ships with two triggers ChangeInSourceControlTrigger and SuccessfulBuildTrigger.

build_necessary? method : project.rb

ChangeInSourceControlTrigger : change_in_source_control_trigger.rb

Plugins

Project
plugin add_plugin method_missing notify plugins respond_to?
BuildReaper
build_finished
EmailNotifier
build_finished build_fixed
BuilderStatus
build_finished build_initiated build_loop_failed build_requested polling_source_control queued sleeping timed_out
ProjectLogger
build_finished build_loop_failed build_started new_revisions_detected no_new_revisions_detected polling_source_control sleeping

Each project has a set of plugins which are notified of events in the build sequence. A plugin can respond to these events by defining a method of the same name as the event. The set of possible events is dynamic and events are despatched from several places including the project build methods, and the triggers. Some of the events include

notify method : project.rb

E-mail notifier plugin : email_notifier.rb

Source Control

Project
source_control update_project_to_revision
Subversion
check_externals checkout clean_checkout latest_revision up_to_date? update

The build system interacts with the source control using three methods latest_revision(project), revisions_since(project, revision_number) and update(project, revision = nil). The add project script also requires a checkout(revision = nil) method.

Cruise Control currently ships with only a Subversion source control manager, but git and mercurial support are in the works and are available from the projects git repository.

update_to_latest_revision method : project.rb

Subversion client : subversion.rb