DRY your CruiseControl.NET Configuration

January 30, 2009 - 5 minute read -
continuous integration cruisecontrol Automation

Don't Repeat Yourself (DRY) is one of the principles of good software development. The idea is that there should ideally be one and only one "source of knowledge" for a particular fact or calculation in a system. Basically it comes down to not copying-and-pasting code around or duplicating code if at all possible. The advantages of this are many.

Advantages of DRY

  • There will be less code to maintain
  • If a bug is found, it should only have to be fixed in one place
  • If an algorithm or process is changed, it only needs to be changed in one place
  • More of the code should become reusable because as you do this you will parameterize methods to make them flexible for more cases

If it's good for code isn't it good for other things like configuration? Why yes it is.

Using CruiseControl.NET Configuration Builder

The Configuration Preprocessor allows you to define string properties and full blocks of XML to use for substitution and replacement. To start using the Configuration Preprocessor, you add xmlns:cb="urn:ccnet.config.builder", an xml namespace, to your document to tell the config parser that you plan to do this.

From there you can define a simple property like:

<cb:define client="xxx"/>

Or you can make it a full block of XML:

<cb:define name="svn-block">
    <sourcecontrol type="svn"></p>
<trunkUrl>http://svn.example.com/svn/$(client)/$(project)/trunk</trunkUrl>
        <workingDirectory>D:\Builds-Net\projects\$(client)\$(project)\trunk</workingDirectory>
        <executable>svn.exe</executable>
        <autoGetSource>true</autoGetSource>
    </sourcecontrol>
</cb:define>

Defining Reusable Blocks

Using these ideas I wanted to come up with a templated approach that would allow me to share configuration among multiple projects. That way, if I added new statistics or change the layout of my build server, I would only have to change it in a single place. Thus keeping things DRY. It also encourages more consistency across multiple projects making things easier to understand.

So, I started defining some reusable blocks in the main ccnet.config file which you can see below. The exact details will depend on your configuration of course.

Full Example of config.xml

<!--
How to add a new project:
Step 1. Create a config file named "<config>-project.xml"
Step 2. Add the project reference below
--></p>
<p><cruisecontrol xmlns:cb="urn:ccnet.config.builder"></p>
<p>    <!-- cb defines to compose reusable blocks of configuration -->
    <!-- use <cb:define client="xxx"/> and <cb:define project="yyy"/> to use these --></p>
<p>    <cb:define name="svn-block">
        <sourcecontrol type="svn"></p>
<trunkUrl>http://svn.example.com/svn/$(client)/$(project)/trunk</trunkUrl>
            <workingDirectory>D:\Builds-Net\projects\$(client)\$(project)\trunk</workingDirectory>
            <executable>svn.exe</executable>
            <autoGetSource>true</autoGetSource>
        </sourcecontrol>
    </cb:define></p>
<p>    <cb:define name="msbuild-20-block">
        <msbuild>
            <executable>C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\MSBuild.exe</executable>
            <workingDirectory>D:\Builds-Net\projects\$(client)\$(project)\trunk</workingDirectory></p>
<projectFile>build.proj</projectFile>
            <buildArgs>$(build-args) </buildArgs>
            <targets>$(build-targets)</targets>
            <timeout>600</timeout>
            <logger>D:\Program Files\CruiseControl.NET\server\Rodemeyer.MsBuildToCCNet.dll</logger>
        </msbuild>
    </cb:define></p>
<p>    <cb:define name="msbuild-35-block">
        <msbuild>
            <executable>C:\WINDOWS\Microsoft.NET\Framework\v3.5\MSBuild.exe</executable>
            <workingDirectory>D:\Builds-Net\projects\$(client)\$(project)\trunk</workingDirectory></p>
<projectFile>build.proj</projectFile>
            <buildArgs>$(build-args) </buildArgs>
            <targets>$(build-targets)</targets>
            <timeout>600</timeout>
            <logger>D:\Program Files\CruiseControl.NET\server\Rodemeyer.MsBuildToCCNet.dll</logger>
        </msbuild>
    </cb:define></p>
<p>    <cb:define name="merge-block">
        <!-- Merge the output of tests, code coverage and fxcop -->
        <merge>
            <files>
                <file>D:\Builds-Net\projects\$(client)\$(project)\trunk\*.Test.xml</file>
                <file>D:\Builds-Net\projects\$(client)\$(project)\trunk\*.CoverageMerge.xml</file>
                <file>D:\Builds-Net\projects\$(client)\$(project)\trunk\*.CoverageSummary.xml</file>
                <file>D:\Builds-Net\projects\$(client)\$(project)\trunk\*.FxCop.xml</file>
            </files>
        </merge>
    </cb:define></p>
<p>    <cb:define name="loggers-block">
        <xmllogger>
            <logDir>D:\Builds-Net\projects\$(client)\$(project)\logs</logDir>
        </xmllogger>
        <rss/>
        <modificationHistory  onlyLogWhenChangesFound="true" />
    </cb:define></p>
<p>    <cb:define name="stats-block">
        <statistics>
            <statisticList>
                <firstMatch name="Svn Revision" xpath="//modifications/modification/changeNumber" />
                <firstMatch name="Coverage" xpath="//coverageReport/project/@coverage" generateGraph="true"/>
                <firstMatch name="Warnings" xpath="//msbuild/@warning_count" generateGraph="true"/>
                <firstMatch name="Errors" xpath="//msbuild/@error_count" generateGraph="true"/></p>
<p>                <!-- NDepend -->
                <!--
                <firstMatch name="ILInstructions" xpath="//ApplicationMetrics/@NILInstruction" />
                <firstMatch name="Avgerage Complexity" xpath="//ApplicationMetrics/MethodCC/@Avg" />
                <firstMatch name="Max Complexity" xpath="//ApplicationMetrics/MethodCC/@MaxVal" />
                <firstMatch name="LinesOfCode" xpath="//ApplicationMetrics/@NbLinesOfCode" generateGraph="true"/>
                <firstMatch name="LinesOfComment" xpath="//ApplicationMetrics/@NbLinesOfComment" generateGraph="true"/>
                -->
            </statisticList>
        </statistics>
    </cb:define></p>
<p>    <cb:include href="config-client-project.xml"/>
    <cb:include href="config-client2-project-trunk.xml"/>
</cruisecontrol>

At the end of the file you can see the cb:include references. Those are one-line includes to include the configuration of each project. This makes things easier to manage, I think, because you only have to look at the individual project configuration.

Using Reusable Blocks in Individual Configuration Files

From there I need to make use of those defined blocks in in individual file. The first thing I needed to do was to set the parameters that I had defined as simple string replacements in the reusable blocks. Normally you would do that with cb:define as I showed above. But the trick is that you can only have one property with a given name defined. If you include multiple project configurations that doesn't work. What does work is using cb:scope definitions. This allows for a value to be defined only within a specific scope.

<cb:scope
         client="ExampleClient"
         project="SpecialProject"
         build-args="/p:Configuration=Debug"
         build-targets="Clean;Test">
 ...
</cb:scope>

From there you just need to start including the blocks that you defined in the main ccnet.confg within the scope block.

Full Example of Project Configuration

</p>
<p><!-- CruiseControl.NET configuration --></p>
<project name="ExampleClient SpecialProject" xmlns:cb="urn:ccnet.config.builder">
<p>    <cb:scope
         client="ExampleClient"
         project="SpecialProject"
         build-args="/p:Configuration=Debug"
         build-targets="Clean;Test"></p>
<p>        <cb:svn-block/></p>
<p>        <tasks>
            <cb:msbuild-35-block/>
        </tasks></p>
<publishers>
<p>            <cb:merge-block/></p>
<p>            <!-- Enable collection of project statistics -->
            <cb:stats-block/></p>
<p>            <cb:loggers-block/></p>
<p>            <email mailhost="smtp.example.com" from="[email protected]" includeDetails="true">
                <users>
                    <user name="Developer One" group="buildmaster" address="[email protected]" />
                    <user name="Developer Two" group="developers" address="[email protected]" />
                </users>
                <groups>
                    <group name="developers" notification="change" />
                    <group name="buildmaster" notification="change" />
                </groups>
            </email>
        </publishers>
    </cb:scope>
</project>

As you can see, the only one I didn't template out was the email block because that depends on the developers working on each project.

Have fun bringing simplicity and consistency to your Cruise Control.NET configuration!

For the full details see the CruiseControl.NET Configuration Preprocessor documentation.