CCNet vs. CruiseControl

Set once and let it go, this is how Continuous Integration works for automated build process along with development cycle. For a large number of projects, the maintenance for such continously integrated build environment could be complicate and cumbersome. This article will provide and discuss sample config files from practice experience to make easy using CruiseControl.NET and CruiseControl with subversion repository. For comparison of all other similar products, see ThoughtWorks CI Feature Matrix.

Contents


CruiseControl.NET

CCNET (CruiseControl.NET) starts with ccnet.config:

<!--ccnet.config-->
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE cruisecontrol SYSTEM "file:ccnet_definitions.dtd">

<cruisecontrol xmlns:cb="urn:ccnet.config.builder">
<cb:include href="ccnet_definitions.xml" xmlns:cb="urn:ccnet.config.builder"/>

<queue name="$(computername)" duplicates="ApplyForceBuildsReAdd" />
<cb:include href="ccnet_Project.Standard_Solution.xml" xmlns:cb="urn:ccnet.config.builder"/>

<cb:include href="ccnet_MyProject.xml" xmlns:cb="urn:ccnet.config.builder"/>
</cruisecontrol>

In order to maintain CCNET config and projects in clear XML code, the ccnet.config in above example takes advantage of CCNET powerful preprocessor features. There are three types of CCNET configuration preprocessors: constant (including text/variable and nodeset XML fragment), nested expansion (defining a class whose instance can take parameters), and include file (<cb:include />).

Including external config files helps putting constant/variable definitions, project definitions, and reusable XML pieces together in an organized structure, so that ccnet.config can be read in a from-top-to-down order while presenting high-level view in the main config. Another advanced use of include files is to allow editing definition or project file individually without touching ccnet.config. CCNET has the ability to notice any include file changed and reload ccnet.config automatically.

Like writing a program, the first thing in design is to define constants, variables, and reusable functions (or templates) that can be shared across the projects. These are all implemented by <cb:define /> in CCNET. Constants and XML fragments can be defined by DTD, such as in XHTML DTDs and following sample of ccnet_definitions.dtd included in ccnet.config.

<!--ccnet_definitions.dtd-->
<!ENTITY dir_ccnet "$(ProgramFiles)\CruiseControl.NET">
<!ENTITY dir_ccnet_server "$(ProgramFiles)\CruiseControl.NET\server">
<!ENTITY dir_ccnet_artifacts "$(ProgramFiles)\CruiseControl.NET\server\projects">
<!ENTITY dir_svn "$(ProgramFiles)\CollabNet Subversion Server">
<!ENTITY dir_svn_target "\\10.0.1.100\svnbuilds">
<!ENTITY dir_dotnet "$(SystemRoot)\Microsoft.NET\Framework\v3.5">
<!ENTITY dir_msvs "$(ProgramFiles)\Microsoft Visual Studio 9.0">

<!ENTITY svn_server "svn://192.168.0.100">
<!ENTITY auth_svn "<username>svn_user</username><password>svn_password</password>" >

<!ENTITY exec_devenv "<executable>&dir_msvs;\Common7\IDE\devenv.com</executable>" >
<!ENTITY exec_nmake "<executable>&dir_msvs;\VC\bin\nmake.exe</executable>" >
<!ENTITY exec_msbuild "<executable>&dir_dotnet;\MSBuild.exe</executable>" >
<!ENTITY exec_svn "<executable>&dir_svn;\svn.exe</executable>" >

<!ENTITY url_ccnet "http://localhost/ccnet">

However, entity definition has its limitation in use (such as in string value of a property) and modifying a system file included by <!DOCTYPE > cannot trigger ccnet.config to reload. In CCNET, <cb:define /> is recommended to perform the same and even better job. The following sample illustrates syntax of defining text constant (variable) and nodeset (xml fragment). The third usage of <cb:define />, nested expansion, is for reusable class that can take parameters by instance, such as defining a project template.

<!--ccnet_definitions.xml-->
<cb:config-template xmlns:cb="urn:ccnet.config.builder">
<!--# Preprocessor: Text Constants -->
<cb:define const_name="value" /> <-- defines $(const_name), or <cb:const_name/> -->
<cb:define dir_ccnet="$(ProgramFiles)\CruiseControl.NET" />
<cb:define dir_ccnet_server="$(dir_ccnet)\server" />
<cb:define dir_ccnet_artifacts="$(dir_ccnet_server)\projects" />
<cb:define dir_ccnet_buildlogger="$(dir_ccnet)\server\ThoughtWorks.CruiseControl.MsBuild.dll" />
<cb:define dir_dotnet="$(SystemRoot)\Microsoft.NET\Framework\v3.5" />
<cb:define dir_svn="$(ProgramFiles)\CollabNet Subversion Server" />
<cb:define dir_svn_source="c:\svn_checkout" />
<cb:define dir_svn_builds="d:\svn_builds" />
<cb:define dir_svn_target="\\10.0.1.100\svnbuilds" />
<cb:define dir_system32="$(SystemRoot)\system32" />
<cb:define dir_msvs="$(ProgramFiles)\Microsoft Visual Studio 9.0" />

<cb:define svn_username="svn_username" />
<cb:define svn_password="svn_password" />

<cb:define url_svn_target="file://///10.0.1.100/svnbuilds" />
<cb:define url_ccnet="http://localhost/ccnet" />

<!--# Preprocessor: Nodeset Constants -->
<cb:define name="xml_default_extlink">
<externalLink name="CCNET Builds [Ctrl+Click to open in Explorer]" url="$(url_svn_target)" />
</cb:define>
<cb:define name="xml_logger">
<xmllogger logDir="$(dir_ccnet_server)\projects\$(project)" />
</cb:define>

<cb:define name="auth_svn">
<username>$(svn_username)</username>
<password>$(svn_password)</password>
</cb:define>
<cb:define name="exec_devenv">
<executable>$(dir_msvs)\Common7\IDE\devenv.com</executable>
</cb:define>
<cb:define name="exec_nmake">
<executable>$(dir_msvs)\VC\bin\nmake.exe</executable>
</cb:define>
<cb:define name="exec_vcvars">
<executable>$(dir_msvs)\VC\vcvarsall.bat</executable>
<baseDirectory>$(dir_msvs)\VC</baseDirectory>
<buildArgs>x86</buildArgs>
</cb:define>
<cb:define name="exec_msbuild">
<executable>$(dir_dotnet)\MSBuild.exe</executable>
</cb:define>
<cb:define name="exec_svn">
<executable>$(dir_svn)\svn.exe</executable>
</cb:define>

<!--# Preprocessor: Nested Expansions (see other ccnet_*.xml)
<cb:define name="xml_element"><some_element property="$(var1)" /><more>$(var2)</more></cb:define>
<cb:xml_element var1="value1" var2="value2" />
-->

</cb:config-template>
The use of definitions in CCNET is easy to understand from above sample config file. First of all, any system environment variables can be referenced by $(env_var), where $(env_var) is an environment variable accessible in CCNET runtime context. For example, if CCNET is started by ccnet.exe manually in a Command Prompt window, all system variables and logon user variables should be available; otherwise, if CCNET is started in service.msc, only system variables can be used.

Once a CCNET constant/variable (<cb:define var_name="text" />) or XML fragment (<cb:define name="element_name" >...</cb:define>) is defined, it can be referenced immediately afterward thru whole CCNET runtime environment. The constant/variable can be referenced as a string by $(var_name), or <cb:var_name />. The XML fragment must be referenced by <cb:element />. All <cb:define /> are global definitions. To control the scope of a preprocessor definition, use <cb:scope />. See configuration preprocessors.

Next, let's take a look at how to use CCNET nested expansions and parameters of preprocessor definition to create a project template. This template can be used to meet the following conditions:
  • All project properties can be initiated by an instance of the template
  • There is only one build target path needed to be published
  • There is only one developer and who will be in notification for all build states
  • The project can be built by a MS Visual Studio 2008 solution file
Similar to ccnet_definitions.xml, ccnet_Project.Standard_Solution.xml uses <cb:config-template /> but only defines one XML fragment named "project_template_solution".

<!--ccnet_Project.Standard_Solution.xml-->
<?xml version="1.0" encoding="utf-8" ?>
<cb:config-template xmlns:cb="urn:ccnet.config.builder">
<cb:define name="project_template_solution">
<project name="$(project_name)" queue="$(project_queue)" queuePriority="0">
<category>$(project_category)</category>
<workingDirectory>$(dir_svn_source)\$(project_name)</workingDirectory>
<artifactDirectory>$(dir_ccnet_artifacts)\$(project_name)</artifactDirectory>
<webURL>$(url_ccnet)</webURL>

<labeller type="svnRevisionLabeller">
<prefix>$(project_name)-r</prefix>
<major>0</major>
<minor>0</minor>
<url>svn://$(project_svnServer)$/(project_Root)$(project_Path)/$(project_Branch)</url>
<cb:auth_svn />
</labeller>
<!--
<labeller type="lastChangeLabeller"><prefix>$(project_name)-</prefix></labeller>
-->
<triggers>
<!--NOTE: build machine must sync clock with svn server!!!-->
<intervalTrigger buildCondition="IfModificationExists" seconds="$(project_checkTime)" initialSeconds="60" />
</triggers>
<sourcecontrol type="svn">
<trunkUrl>svn://$(project_svnServer)$/(project_Root)$(project_Path)/$(project_Branch)</trunkUrl>
<cb:auth_svn />
<cb:exec_svn />
<workingDirectory>$(dir_svn_source)\$(project_name)</workingDirectory>
<timeout units="seconds">$(project_checkTime)</timeout>
</sourcecontrol>

<tasks>
<msbuild>
<cb:exec_msbuild />
<projectFile>$(project_solution)</projectFile>
<workingDirectory>$(dir_svn_source)\$(project_name)</workingDirectory>
<buildArgs>/noconsolelogger /p:Configuration=Release</buildArgs>
<logger>$(dir_ccnet_buildlogger)</logger>
</msbuild>
</tasks>

<publishers>
<cb:xml_logger project="$(project_name)" />

<buildpublisher>
<sourceDir>$(dir_svn_source)\$(project_name)\$(project_buildTarget)</sourceDir>
<publishDir>$(dir_svn_target)</publishDir>
<useLabelSubDirectory>true</useLabelSubDirectory>
<alwaysPublish>false</alwaysPublish>
</buildpublisher>

<cb:include href="ccnet_email.xml" xmlns:cb="urn:ccnet.config.builder" />
<cb:ccnet_email
developer_name="Developer" developer_alias="$(project_developer)"
notification="Always"
/>
</publishers>

</project>
</cb:define>

</cb:config-template>

Different from ccnet_definitions.xml, ccnet_Project.Standard_Solution.xml also uses some variables that have never been defined, such as $(project_name), $(project_path), and etc., basically anything with "project_" prefix in this example. This is the exclusive feature of CCNET preprocessor that allow all these variable dynamically bound to an instance of the template when it is used. In ccnet.config, after ccnet_Project.Standard_Solution.xml is loaded, a project file ccnet_MyProject.xml comes after to initiate a project configuration based on the template. Here is code:

<!--ccnet_MyProject.xml-->
<cb:config-template xmlns:cb="urn:ccnet.config.builder">
<cb:project_template_solution
project_name="My Project"
project_alias="myproject"
project_category="My Category"
project_path="MyProject"
project_root=""
project_branch="trunk"
project_svnServer="192.168.10.100"
project_svnPath="svn://192.168.10.100/Software/MyProject/trunk"
project_solution="MyProject.sln"
project_buildTarget="Setup\Release"
project_buildTime="1800"
project_checkTime="600"
project_developer="myalias"
project_queue="$(computername)"
/>

</cb:config-template>

The properties defined in "My Project" configuration makes the template used in complete. Actually, the property names after cb:project_template_solution may not be necessary matching what have been defined in the template. At least CCNET does not check if a referenced constant or variable is predefined. Undefined constant/variable will be replaced by empty string (- be caution that this could cause exception at the runtime). And any property defined in the instance but not used in the template will be ignored (e.g. project_svnPath).

CCNET preprocessor config template can be nested. Here is the file of ccnet_email.xml that has been included in ccnet_Project.Standard_Solution.xml:

<!--ccnet_email.xml-->
<cb:config-template xmlns:cb="urn:ccnet.config.builder">

<cb:define name="ccnet_email">
<email from="Builder@mycompany.com"
mailhost="192.168.10.25" includeDetails="TRUE" useSSL="FALSE"
subjectPrefix="CCNET:" description="CruiseControl.NET E-mail Notification ">
<users>
<user name="Builder" group="Builder" address="builder@mycompany.com"/>
<user name="$(developer_name)" group="Developer" address="$(developer_alias)@mycompany.com"/>
<user name="Build Request" group="Supervisor" address="buildrequest@mycompany.com"/>
<user name="Admin" group="Admin" address="admin@mycompany.com"/>
</users>
<groups>
<group name="Admin" notification="Failed"/>
<group name="Builder" notification="Success"/>
<group name="Developer" notification="$(notification)"/>
</groups>

<modifierNotificationTypes>
<NotificationType>Always</NotificationType>
<NotificationType>Failed</NotificationType>
<NotificationType>Fixed</NotificationType>
<NotificationType>Success</NotificationType>
<NotificationType>Change</NotificationType>
</modifierNotificationTypes>

<subjectSettings>
<subject buildResult="Broken" value="Build Failed - ${CCNetProject} - [Broken] :: ${CCNetBuildCondition}" />
<subject buildResult="StillBroken" value="Build Failed - ${CCNetProject} - [StillBroken] :: ${CCNetBuildCondition}" />
<subject buildResult="Fixed" value="Build Success - ${CCNetProject} - [Fixed] :: ${CCNetBuildCondition}" />
<subject buildResult="Exception" value="Build Failed - ${CCNetProject} - [Exception] :: ${CCNetBuildCondition}" />
<subject buildResult="Success" value="Build Success - ${CCNetProject} - [Success] :: ${CCNetBuildCondition}" />
</subjectSettings>
</email>
</cb:define>

<!--# after <cb:include href="ccnet_email.xml" />, define following properties:
developer_name - developer name
developer_alias - developer email alias
notification - notification type: Always|Change|Failed|Success|Fixed
<cb:ccnet_email
developer_name=""
developer_alias=""
notfication=""
/>
==-->
</cb:config-template>
For label variables used in <subjectSettings />, please refer to CCNET Email Publisher.

The last thing left uncovered in the project template is "svnRevisionLabeller". This is a modified plugin based on original svnrevisionlabeller. The code (SvnRevisonLabeller.cs) is attached below:

/**
****************************************
* Class Name: SvnRevisionLabeller
* Description: Subversion functions used in NAnt
* Requisite: Reference to NAnt.Core.dll
* Notes: See http://code.google.com/p/svnrevisionlabeller/
****************************************
*/
using System;
using System.Xml;
using Exortech.NetReflector;
using ThoughtWorks.CruiseControl.Core;
using ThoughtWorks.CruiseControl.Core.Util;

namespace ccnet.SvnRevisionLabeller.plugin
{
/// <summary>
/// Generates label numbers using the Subversion revision number.
/// </summary>
/// <remarks>
/// This class was inspired by Jonathan Malek's post on his blog
/// (<a href="http://www.jonathanmalek.com/blog/CruiseControlNETAndSubversionRevisionNumbersUsingNAnt.aspx">CruiseControl.NET and Subversion Revision Numbers using NAnt</a>),
/// which used NAnt together with Subversion to retrieve the latest revision number. This plug-in moves it up into
/// CruiseControl.NET itself, so that you can see the latest revision number appearing in CCTray. The label can
/// then be retrieved from within NAnt by accessing the property <c>${CCNetLabel}</c>.
/// </remarks>
[ReflectorType("svnRevisionLabeller")]
public class SvnRevisionLabeller : ILabeller
{
#region Private members

private int major;
private int minor;
private string _url;
private string executable;
private string prefix;
private string username;
private string password;
private const string RevisionXPath = "/log/logentry/@revision";

#endregion

#region Constructors

/// <summary>
/// Initializes a new instance of the <see cref="SvnRevisionLabeller"/> class.
/// </summary>
public SvnRevisionLabeller()
{
major = 1;
minor = 0;
executable = "svn.exe";
prefix = String.Empty;
}

#endregion

#region Public methods

/// <summary>
/// Returns the label to use for the current build.
/// </summary>
/// <param name="resultFromLastBuild">IntegrationResult from last build used to determine the next label</param>
/// <returns>the label for the new build</returns>
public string Generate(IIntegrationResult resultFromLastBuild)
{
// Get the last revision from the Subversion repository
int svnRevision = GetRevision();

// Get the last revision from CruiseControl
Version version = ParseVersion(svnRevision, resultFromLastBuild);

// If the revision number hasn't changed (because no new check-ins have been made), increment the build number;
// Otherwise, reset the build number to 0
int revision = (svnRevision == version.Build) ? version.Revision + 1 : 0;

// Construct a new version number, adding any specified prefix
Version newVersion = new Version(major, minor, svnRevision, revision);

if (major == 0 && minor == 0)
{
// <major>0</major><minor>0</minor> must be explicitly defined
// assume we only care about subversion revision - 2008-11-18 jzhu@zetron.com
return prefix + svnRevision;
}
else // keep original usage for a 4-field version format
{
return prefix + newVersion;
}
}

/// <summary>
/// Runs the task, given the specified <see cref="IIntegrationResult"/>, in the specified <see cref="IProject"/>.
/// </summary>
/// <param name="result"></param>
public void Run(IIntegrationResult result)
{
result.Label = Generate(result);
}

#endregion

#region Public properties

/// <summary>
/// Gets or sets the major build number.
/// </summary>
/// <value>The major build number.</value>
[ReflectorProperty("major", Required=false)]
public int Major
{
get
{
return major;
}
set
{
major = value;
}
}

/// <summary>
/// Gets or sets the minor build number.
/// </summary>
/// <value>The minor build number.</value>
[ReflectorProperty("minor", Required=false)]
public int Minor
{
get
{
return minor;
}
set
{
minor = value;
}
}

/// <summary>
/// Gets or sets the repository URL from which the <c>svn log</c> command will be run.
/// </summary>
/// <value>The repository.</value>
[ReflectorProperty("url", Required = true)]
public string Url
{
get
{
return _url;
}
set
{
_url = value;
}
}

/// <summary>
/// Gets or sets the Subversion client executable.
/// </summary>
/// <value>The path to the executable.</value>
/// <remarks>
/// If the value is not supplied, the task will expect to find <c>svn.exe</c> in the <c>PATH</c> environment variable.
/// </remarks>
[ReflectorProperty("executable", Required=false)]
public string Executable
{
get
{
return executable;
}
set
{
executable = value;
}
}

/// <summary>
/// Gets or sets an optional prefix for the build label.
/// </summary>
/// <value>A string to prefix the version number with.</value>
[ReflectorProperty("prefix", Required=false)]
public string Prefix
{
get
{
return prefix;
}
set
{
prefix = value;
}
}

/// <summary>
/// Gets or sets the username to access SVN repository.
/// </summary>
/// <value>The repository.</value>
[ReflectorProperty("username", Required = false)]
public string Username
{
get
{
return username;
}
set
{
username = value;
}
}

/// <summary>
/// Gets or sets the password to access SVN repository.
/// </summary>
/// <value>The repository.</value>
[ReflectorProperty("password", Required = false)]
public string Password
{
get
{
return password;
}
set
{
password = value;
}
}

#endregion

#region Private methods

/// <summary>
/// Parses the version.
/// </summary>
/// <param name="revision">The revision.</param>
/// <param name="resultFromLastBuild">The result from last build.</param>
private Version ParseVersion(int revision, IIntegrationResult resultFromLastBuild)
{
try
{
string label = resultFromLastBuild.LastSuccessfulIntegrationLabel;
if (prefix.Length > 0)
{
label = label.Replace(prefix, String.Empty).TrimStart('_');
}
return new Version(label);
}
catch (SystemException)
{
return new Version(major, minor, revision, 0);
}
}

/// <summary>
/// Gets the latest Subversion revision by checking the last log entry.
/// </summary>
private int GetRevision()
{
// Set up the command-line arguments required
ProcessArgumentBuilder argBuilder = new ProcessArgumentBuilder();
argBuilder.AppendArgument("log");
argBuilder.AppendArgument("--xml");
argBuilder.AppendArgument("--limit 1");
argBuilder.AppendArgument(Url);
if (Username != null && Username.Length > 0 && Password != null && Password.Length > 0)
{
AppendCommonSwitches(argBuilder);
}

// Run the svn log command and capture the results
ProcessResult result = RunProcess(argBuilder);
Log.Debug("Received XML : " + result.StandardOutput);

// Load the results into an XML document
XmlDocument xml = new XmlDocument();
xml.LoadXml(result.StandardOutput);

// Retrieve the revision number from the XML
XmlNode node = xml.SelectSingleNode(RevisionXPath);
return Convert.ToInt32(node.InnerText);
}

/// <summary>
/// Appends the arguments required to authenticate against Subversion.
/// </summary>
/// <param name="buffer"><The argument builder./param>
private void AppendCommonSwitches(ProcessArgumentBuilder buffer)
{
buffer.AddArgument("--username", Username);
buffer.AddArgument("--password", Password);
buffer.AddArgument("--non-interactive");
buffer.AddArgument("--no-auth-cache");
}

/// <summary>
/// Runs the Subversion process using the specified arguments.
/// </summary>
/// <param name="arguments">The Subversion client arguments.</param>
/// <returns>The results of running the process, including captured output.</returns>
private ProcessResult RunProcess(ProcessArgumentBuilder arguments)
{
ProcessInfo info = new ProcessInfo(executable, arguments.ToString(), null);
Log.Debug("Running Subversion with arguments : " + info.Arguments);

ProcessExecutor executor = new ProcessExecutor();
ProcessResult result = executor.Execute(info);
return result;
}
#endregion
}
}


CruiseControl

Comparing to CCNET, CruiseControl does not provide very powerful preprocessor and template feature. There is no syntax to define XML fragment (except using DTD). The main config file, config.xml, cannot be automatically reloaded if any configuration changed.

<!--config.xml-->
<?xml version="1.0" encoding="utf-8" ?>
<!--# Always deploy ccnet.config with following files under ${env.CCDIR}
config.properties
cc_antpublisher_copy.xml
prj_*.xml
also place cruisecontrol.jar under "${env.CCDIR}\lib" to overwrite
original one for a customized new plugin: <ZSVNLabeller />
==-->
<cruisecontrol>
<property file="config.properties" />
<property environment="env" toupper="true" />
<property name="url_cruisecontrol" value="${url_cchost}/cruisecontrol" />
<property name="url_buildresults" value="${url_cruisecontrol}/buildresults" />
<property name="url_ccdashboard" value="${url_cchost}/dashboard" />
<property name="url_ccdoc" value="${url_cchost}/documentation" />

<include.projects file="prj_MyProject.xml" />

</cruisecontrol>

As in above sample of config.xml, property definition is either by <property name="var" value="text" /> element or an external file (e.g. config.properties).

###############################################################################
# Filename: config.properties
# Usage: called by cruisecontrol element, i.e.:
# <property file="config.properties" />
# Syntax: defined by the class java.util.Properties, with the same rules
# about how non-ISO8859-1 characters must be escaped.
###############################################################################

### default value predefinitions
def_hostip=10.0.1.194
def_hostname=hostname
def_project_buildTime=1800
def_project_checkTime=600

### dir/path predefinitions
dir_cruisecontrol=%ProgramFiles%\CruiseControl
dir_svn_target=\\10.0.1.100\svnbuilds
pwd_cruisecontrol=/srv/cruisecontrol-bin-2.8.2
smb_svn_target=/svnbuilds

### svn settings predefinitions
svn_server=svn://192.168.10.100
svn_username=svn_username
svn_password=svn_password

### user id predefinitions
uid_admin=admin
uid_builder=builder

### url predefinitions
url_cchost=http://hostname:8080
url_ccdashboard=http://hostname:8080/dashboard
url_ccdoc=http://hostname:8080/documentation
url_cruisecontrol=http://hostname:8080/cruisecontrol
url_buildresults=http://hostname:8080/cruisecontrol/buildresults
url_ccnet=http://localhost/ccnet

Certainly, a project file (e.g. prj_MyProject.xml) can be included in config.xml so that the configuration is well organized by each individual config file. Here only provides a project template file that a real project config (e.g. prj_MyProject.xml) will be based on.

<!--prj_project.template.xml-->
<?xml version="1.0" encoding="utf-8" ?>
<!--#
For each project, use following steps to create a project file from this template by
copying this file to a new project file, in convention of "prj_$[project_name].xml":
(1) replace "$[project_name]" with a project name
(2) replace any place holder definition, like in format of $[place_holder], in property definitions
(3) optionally use ${def_project_buildTime} for ${project_buildTime}
(4) optionally add more ${project_buildTarget[n]} properties and publishers
(5) choose one of the build methods in <project><schedule></schedule></project> block
(6) replace "\\" to "/" on Linux system
(7) replace ${dir_svn_target} with ${smb_svn_target} on a Linux build system
(8) define properties ${eid_*} for build notification; ${uid_builder} has been notified on success
(9) refer to any property definitions in config.properties
==-->
<cruisecontrol>
<project name="$[project_name]">
<property name="project_name" value="${project.name}" />
<property name="project_alias" value="$[project_alias]" />
<property name="project_category" value="$[project_category]" />
<property name="project_root" value="$[project_root]" />
<property name="project_path" value="$[project_path]" />
<property name="project_branch" value="$[project_branch]" />
<property name="project_svnServer" value="$[project_svnServer]" />
<property name="project_svnPath" value="svn://${project_svnServer}/${project_root}${project_path}/${project_branch}" />
<property name="project_source" value="${env.CCDIR}\\projects\\${project.name}" />
<property name="project_buildTarget" value="$[project_buildTarget]" />
<property name="project_buildTime" value="${def_project_buildTime}" />
<property name="project_buildTime" value="$[project_buildTime]" />
<property name="project_checkTime" value="30" />
<property name="url_cruisecontrol" value="${url_cruisecontrol}" />
<property name="eid_failure" value="builder" />
<property name="eid_success" value="builder" />
<property name="eid_always" value="developer_alias" />

<plugin name="svn" classname="net.sourceforge.cruisecontrol.sourcecontrols.SVN" />
<plugin name="ZSVNLabeller" classname="net.sourceforge.cruisecontrol.labelincrementers.ZSVNLabeller" />

<ZSVNLabeller separator="r" labelprefix="r" workingcopypath="${project_source}"
/>
<!--#
<listeners>
<currentbuildstatuslistener file="logs/${project.name}/status.txt"/>
</listeners>
==-->
<bootstrappers>
<!--#
<antbootstrapper anthome="apache-ant-1.7.0"
buildfile="projects/${project.name}/build.xml"
target="clean" />
<execbootstrapper command=""
args="" workingdir="projects/${project.name}"
errorstr="" showProgress=""
timeout=""
/>
==-->
<svnbootstrapper localWorkingCopy="${project_source}"
username="${svn_username}"
password="${svn_password}"
/>
</bootstrappers>

<modificationset quietperiod="60">
<!--# <filesystem/> triggers a build if any file in ${project.name} project is touched
<filesystem folder="${project_source}" includedirectories="false" />
==-->
<!--# <svn/> sets property ${svnrevision}
==-->
<svn localWorkingCopy="${project_source}"
useLocalRevision="false"
username="${svn_username}"
password="${svn_password}"
property=""
/>
</modificationset>

<schedule interval="{project_checkTime}">
<!--#<ant />
<ant anthome="apache-ant-1.7.0" buildfile="projects/${project.name}/build.xml"/>
==-->
<!--#<exec/>
==-->
<exec command=".\\build.cmd"
args='"${project_source}"' workingdir="${project_source}"
errorstr="Build Failed" showProgress="true"
timeout="${project_buildTime}"
/>
</schedule>

<log>
<!--# merge from log files with specified pattern from specified dir
<merge dir="${project_source}\\log" pattern="*.log" />
==-->
<deleteartifacts every="30" unit="DAY" />
<delete every="30" unit="DAY" />
</log>

<publishers>
<htmlemail buildresultsurl="${url_cruisecontrol}/buildresults/${project_name}"
defaultsuffix=".com" mailhost="192.168.10.66" mailport="25" usessl="false"
returnaddress="builder@mycompany.com" returnname="Builder"
subjectprefix="CruiseControl:"
>
<success address="${uid_builder}@mycompany.com" /><!--send to builder success notification-->
<success address="${eid_success}@mycompany.com" />
<failure address="${eid_failure}@mycompany.com" />
<always address="${eid_always}@mycompany.com" />
</htmlemail>

<onsuccess>
<antpublisher anthome="apache-ant-1.7.0"
buildfile="${env.CCDIR}\\cc_antpublisher_copy.xml"
target="publish_dir"
>
<property name="publish_source" value="${project_source}\\${project_buildTarget}" />
<property name="publish_target" value="${dir_svn_target}\\${project_alias}" />
</antpublisher>
<!--
<antpublisher anthome="apache-ant-1.7.0"
buildfile="${env.CCDIR}\\cc_antpublisher_copy.xml"
target="publish_file"
>
<property name="publish_source" value="${project_source}\\${project_buildTarget}" />
<property name="publish_target" value="${dir_svn_target}\\${project_alias}" />
</antpublisher>
==-->
</onsuccess>
</publishers>

</project>

</cruisecontrol>

The template cannot be used as nicely as in CCNET. But it gives a start for most projects. Please refer to usage comments at beginning of the template. Also remember since CruiseControl is running on both Windows and Linux systems, backslash or forward-slash need to be carefully used wherever a path is typed.

One of the difference in CruiseControl than CCNET is the sequence of continuous integration cycle. In CCNET, the <sourcecontrol /> compares last build state with repository server, then checkout (for the first time of check) or update source code on local working directory before starting build tasks. But in CruiseControl, the bootstrapper is always called at the beginning to update local working directory from server before using <modificationset /> to detect if tasks in <schedule /> need to be executed.

Such difference implies that the local working directory and source code in CruiseControl project must be manually checked out from the repository (such as `svn co`) at the time when the project is setup, which is not necessary in CCNET - where the local working directory can be created automatically if it does not exist. This also explains why LocalWorkingCopy (rather than RepositoryLocation) property should be used in <svn /> source control under <modificationset /> to detect new commit for a CruiseControl project.

Note: There could be a chance that local working copy has not been updated yet when a new change is committed on repository server after bootstrapper but before modificationset. The race condition will end up building old source against a newer revision label.

Modification of subversion in CruiseControl is parsed from `svn log -r` by checking between last build time and current time. If useLocalRevision property is used in <svn /> source control, the local revision number will be in place of current time.

At the end of the template, artifacts publishing is implemented by the following include file.

<!--cc_antpublisher_coppy.xml-->
<project name="antpublisher_copy" default="publish">
<target name="publish">
<echo>Copying ${publish_source} to ${publish_target} ...</echo>
<copy todir="${publish_target}"><fileset dir="${publish_source}" /></copy>
</target>
<target name="publish_dir">
<echo>Copying ${publish_source} to ${publish_target}-${label} ...</echo>
<copy todir="${publish_target}-${label}"><fileset dir="${publish_source}" /></copy>
</target>
<target name="publish_file">
<echo>Copying ${publish_source} to ${publish_target}-${label} ...</echo>
<copy todir="${publish_target}-${label}" file="${publish_source}"></copy>
</target>
<target name="publish_artifacts">
<echo>Copying ${publish_source_dir} to ${publish_target} ...</echo>
<copy todir="${publish_target}">
<fileset dir="${publish_source_dir}">
<include name="${publish_source_files}" />
</fileset>
</copy>
</target>
</project>

The Java source code of "ZSVNLabeller" plugin (based on SVNLabelIncrementer, implements LabelIncrementer) is attached as below. The file "ZSVNLabeller.java" should be place under source code folder "net\sourceforge\cruisecontrol\labelincrementers". After rebuilt, copy "cruisecontrol.jar" to ${env.CCDIR}, where CruiseControl installed.

package net.sourceforge.cruisecontrol.labelincrementers;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

import net.sourceforge.cruisecontrol.LabelIncrementer;

import org.apache.log4j.Logger;
import org.jdom.Element;

/**
* This class provides a label incrementation based on svn revision numbers.
* This class expects the label format to be "x&lt;sep&gt;y[&lt;sep&gt;z]",
* where x is any String and y is an integer and &lt;sep&gt; a separator, the
* last part z, is optional, and gets generated and later incremented in case a
* build is forced. The default separator is "." and can be modified using
* {@link #setSeparator}.
*
* @author Ketan Padegaonkar &lt; KetanPadegaonkar gmail &gt;
*/

/// UPDATE: return revisionNumber if separator is as same as prefix - Boathill

public class ZSVNLabeller implements LabelIncrementer {
private static final Logger LOG = Logger.getLogger(ZSVNLabeller.class);

private String workingCopyPath = ".";

private String labelPrefix = "svn";

private String separator = ".";

public boolean isPreBuildIncrementer() {
return true;
}

public String incrementLabel(String oldLabel, Element buildLog) {
String revisionNumber = "";
String result = oldLabel;
try {
revisionNumber = getSvnRevision();

if (getSeparator().equals(getLabelPrefix())) {
return labelPrefix + revisionNumber; // return svn revison only
}
if (revisionNumber == null || revisionNumber.equals("")) {
return labelPrefix;
}
result = labelPrefix + getSeparator() + revisionNumber;

if (oldLabel.indexOf(result) > -1) {
int lastSeparator = oldLabel.lastIndexOf(getSeparator());
int firstSeparator = oldLabel.indexOf(getSeparator());
int lastPart = 1;
if (lastSeparator != firstSeparator) {
String suffix = oldLabel.substring(lastSeparator + 1);
lastPart = Integer.parseInt(suffix) + 1;
}
result += getSeparator() + lastPart;
}
LOG.debug("Incrementing label from " + oldLabel + " to " + result);
} catch (IOException e) {
LOG.error("could not execute svn binary", e);
} catch (NumberFormatException e) {
LOG.error("could not increment label. Old label was " + oldLabel + ". svn revision was " + revisionNumber,
e);
}

return result;
}

protected String getSvnRevision() throws IOException {
String rev;
Process p = null;
try {
p = Runtime.getRuntime().exec(new String[]{"svnversion", workingCopyPath});
BufferedReader stdInput = new BufferedReader(new InputStreamReader(p.getInputStream()));
rev = stdInput.readLine();
} finally {
if (p != null) {
p.destroy();
}
}
LOG.debug("SVN revision is: " + rev);
return rev;
}

public boolean isValidLabel(String label) {
// we don't mind what the previous label is,
// when the next label is built, then parsing is performed to add / increment a suffix.
return true;
}

public void setWorkingCopyPath(String path) {
LOG.debug("Working Path is: " + path);
workingCopyPath = path;
}

public String getLabelPrefix() {
return this.labelPrefix;
}

public void setLabelPrefix(String labelPrefix) {
this.labelPrefix = labelPrefix;
}

public String getDefaultLabel() {
return getLabelPrefix() + getSeparator() + "0";
}

public String getSeparator() {
return this.separator;
}

public void setSeparator(String separator) {
this.separator = separator;
}
}

Comparison

Some other Continuous Integration products, e.g. Cruise and Hudson, have much nicer web interface, while CruiseControl.NET and CruiseControl provides better flexibility and can be greatly customized. However, the flexibility sometimes means difficulty. For example, CruiseControl has three web sites - dashboard (:8080), project cruisecontrol (:8000), and JMS console - but not be seen on one page; CruiseControl supports CCNET CCTray but could not integrate with CCNET projects or dashboard (even if both developed by ThoughtWorks).

The build publisher supports using build label in both CCNET (by <labeller />) and CruiseControl (${label} in ant config). And in CCNET, the build publisher has to copy all contents in a specified source to another location although ant or nant can be used to customize such task. CruiseControl web interfaces do have a little more advanced features than CCNET. As an instance, the build target files (artifacts) can be published on CruiseControl dashboard so that can be downloaded. But the artifact in CCNET is just a local folder not accessible from the web.

On the other hand, CruiseControl does not display the runtime log on web interfaces. The build log can only be reviewed after the build is done. In CCNET dashboard, this is provided by a server log link. Although the server log in CCNET is not automatically updated (as nicely as a real time console display in Hudson), it at least gives user some hint before knowing the build succeeded or failed. Having better integration with Windows, CCNET can be run in either command line or service mode. For CruiseControl, you have to write a /etc/init.d/cruisecontrol.

Since CruiseControl.NET derived from CruiseControl, both work the same way and the cofiguration shares similarity. As open source projects, it is convenient to develop or modify plugin to suite need during implementation. CruiseControl is cross-platform by Java technology. CCNET has improved on web integration and preprocessor configuration so that project file can be well structured via nodeset expansion template.

Note: Source code samples are formatted by SyntaxHighlighter.

No comments:

Post a Comment