Setup of a continuous integration build

I thought I'd document our continuous integration build, since we had to pull together a number of libraries from different sources in order to get it working.  This is quite a long post, but there was quite a lot to do ...  :-)

Solution background
Our project is currently a .Net 2.0 Windows application which uses some external libraries.  There is a database (obviously) but there is no other back-end or middle tier system (e.g. web services) at present.  Our source code is stored in Subversion (SVN), we are using CruiseControl.Net v1.0.1 (CC.Net) for continuous integration and NUnit for unit tests.

Overview of the build process
We'll get down to the detail of all these steps a bit later, but at a high level our continuous integration process is as follows:

  1. CC.Net is configured to monitor subversion for changes
  2. When it detects some, it assigns a build number and kicks off the main build process
  3. The build process in CC.net has one task, and that is to fire up MSBuild with our build script.  Steps 4-12 are executed within MSBuild.
  4. Existing source code is removed
  5. Our artefact directory is cleaned out (more on this later)
  6. The latest version of source code is obtained from Subversion
  7. The CC.net build label is inserted into an AssemblyVersion attribute in a common AssemblyInfo.cs.  This file is then copied into source directories for all our projects.
  8. The main compilation occurs
  9. NCover is called.  This shells out to NUnit in order to execute all the unit tests and collects code coverage information at the same time.
  10. NCoverExplorer produces some rolled-up coverage statistics which drive a really useful report.
  11. FxCop is called to perform static code analysis.
  12. The compiled binaries are copied to a drop location, in a folder named with the CC.net build label.
  13. CC.net then merges a number of XML files in order to produce the build report.  These XML files are produced by all the various utilities we call (NUnit, FxCop, etc) and are all created in the artefact directory.

We also modified a few other settings and stylesheets within CC.Net.  For the sake of completeness, I talk about these in a separate section at the end of this post.

1. Setting up CC.Net to monitor subversion
Not a very difficult task this, just create an SVN account and add the following into the ccnet.config file:

<sourcecontrol type="svn">
    <
trunkUrl>svn://192.168.104.10/amr/trunk</trunkUrl>
    <
workingDirectory>D:\Projects\MyProject\src</workingDirectory>
    <
executable>C:\Program Files\Subversion\bin\svn.exe</executable>
    <
username>cruise.control</username>
    <
password>ccnet</password>
</
sourcecontrol>

You should replace "cruise.control" and "ccnet" with the username and password of your SVN account.  Note that CC.Net is able to poll SVN for changes, but can't yet checkout the latest source code to perform the build.  We do this from MSBuild in step 6 below.

2. Choosing a build number
We are on an agile project, so the IterationLabeller makes sense for us.  We also use the build label for versioning (step 7 below) and the IterationLabeller also suits that purpose.  Our ccnet.config file contains the following:

<labeller type="iterationlabeller">
    <prefix>1.0</prefix>
    <
duration>4</duration>
    <
releaseStartDate>2006/7/3</releaseStartDate>
    <
separator>.</separator>
</
labeller>

3. Kicking off MSBuild
We decided that MSBuild is a more supported and extensible environment than CC.Net (and also a number of people have written build tasks which we could leverage).  So our <tasks> element in ccnet.config contains a single entry:

<msbuild>
    <
executable>C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\msbuild.exe</executable>
    <
projectFile>ContinuousIntegration.build</projectFile>
    <
buildArgs>/noconsolelogger /p:Configuration=debug /v:n</buildArgs>
    <
targets>BuildTestAnalyse</targets>
    <
timeout>600</timeout>
    <
workingDirectory>D:\Projects\MyProject\Build</workingDirectory
    <
logger>ThoughtWorks.CruiseControl.MsBuild.XmlLogger,D:\Projects\MyProject\Build\CruiseControl.1.0.1\server\ThoughtWorks.CruiseControl.MsBuild.dll</logger>
</
msbuild>

Steps 4 thru 12 occur with the context of MSBuild in the ContinuousIntegration.build file which you see referenced above.

4. Deleting existing source code
Easily done in MSBuild as follows:

<Target Name="CleanSource">
    <
Message Text="Removing all source files from $(SourceDirectory)" />
    <
RemoveDir Directories="$(SourceDirectory)" />
</
Target>

5. Cleaning out the artefact directory
Once again, this is pretty straightforward:

<Target Name="CleanPreviousArtefacts">
    <
Message Text="Cleaning previous XML output files from $(ArtefactDirectory)" />
    <
Delete Files="$(ArtefactDirectory)\*.xml" />
    <
Delete Files="$(ArtefactDirectory)\*.cs" />
</
Target>

6. Getting the latest source code from SVN
This is the first step where we need some third-party code.  The MSBuild Community Tasks Project (http://msbuildtasks.tigris.org/) is the place to go.  This project provides, amongst other things, a number of SvnXxxx tasks are relevant when connecting to SVN.  To include these tasks, copy the downloaded files into a directory somewhere and then include the following in your build script (the path is relative to the build script file):

<Import Project="Tools\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets"/>

The tasks are now available for use.  Our build target looks like this:

<Target Name="GetSource">
    <
Message Text="Checking out trunk into $(SourceDirectory)" />
    <
SvnCheckout RepositoryPath="svn://192.168.104.10/myproject/trunk" 
        LocalPath="$(SourceDirectory)"
        UserName="cruise.control"
        Password="ccnet"
            <
Output TaskParameter="Revision" PropertyName="Revision" />
    </
SvnCheckout>
    <
Message Text="Have got revision: $(Revision)"/>
</
Target>

As with step 1, the username and password should match up to what you created in SVN.

Note: although we get hold of the SVN revision number used for the build, we don't do much with it other than write it into the build log.  I would like to be able to get it onto the front page of our build report and possibly also into some of the assembly properties in our binaries.  For the SVN newbies, the revision number uniquely identifies all the file versions that were used in the build.  It's equivalent to a SourceSafe label.

7. Updating the AssemblyVersionAttribute in AssemblyInfo.cs files
More third party tasks required here - this time we use the AssemblyInfoTask from http://www.gotdotnet.com/codegallery/codegallery.aspx?id=93d23e13-c653-4815-9e79-16107919f93e.  This task loads up an AssemblyInfo.cs file, modifies the assembly-level attributes and then saves it again.  To be honest, it's a little bit sensitive to duplicate attributes in the existing code file but we managed to work around this.  To use the task, download and install the MSI from GotDotNet and insert the following line into your build script:

<Import Project="$(MSBuildExtensionsPath)\Microsoft\AssemblyInfoTask\Microsoft.VersionNumber.targets"/>

We then have our build target as follows:

<Target Name="UpdateCommonFiles">
    <
Copy 
        SourceFiles="$(CommonFileDirectory)\CommonAssemblyInfo.cs" 
        DestinationFiles
="$(ArtifactDirectory)\AssemblyInfo.cs" />
    <
AssemblyInfo AssemblyInfoFiles="$(ArtifactDirectory)\AssemblyInfo.cs"
        AssemblyVersion="$(CCNetLabel)"
        AssemblyFileVersion="$(CCNetLabel)"
        ComVisible="false"
        AssemblyCompany="MyCompany"
        AssemblyConfiguration=""
        AssemblyCopyright="MyProject 2006"
        AssemblyCulture=""
        AssemblyProduct="MyProject"
        AssemblyDelaySign="false"
        AssemblyKeyName=""/>
    <
Copy SourceFiles="$(ArtifactDirectory)\AssemblyInfo.cs" 
        DestinationFolder="$(SourceDirectory)\Common\Properties" />
    <
Copy SourceFiles="$(ArtifactDirectory)\AssemblyInfo.cs" 
        DestinationFolder="$(SourceDirectory)\DataAccess\Properties" />
    <
Copy SourceFiles="$(ArtifactDirectory)\AssemblyInfo.cs" 
        DestinationFolder="$(SourceDirectory)\ReportProcessing\Properties" />
    <
Copy SourceFiles="$(ArtifactDirectory)\AssemblyInfo.cs" 
        DestinationFolder="$(SourceDirectory)\DesktopReporting\Properties" />
</
Target>

You notice how our AssemblyVersion is set to $(CCNetLabel).  Thanks to CC.Net, this is the build label produced by the IterationLabeller we configured in step 2.

For the sake of completeness, here is CommonAssemblyInfo.cs:

using System;
using System.Reflection;
using System.Security.Permissions;
using System.Runtime.InteropServices;

[assembly: ComVisible(false)]
[assembly:
PermissionSet(SecurityAction.RequestMinimum, Name="FullTrust")]
[assembly:
FileIOPermission(SecurityAction.RequestMinimum)]
[assembly:
AssemblyCompany("MyCompany")]
[assembly:
AssemblyConfiguration("")]
[assembly:
AssemblyCopyright("MyCompany 2006")]
[assembly:
AssemblyCulture("")]
[assembly:
AssemblyProduct("MyProject")]
[assembly:
AssemblyTrademark("")]
[assembly:
AssemblyDelaySign(false)]
[assembly:
AssemblyKeyName("")]
[assembly:
CLSCompliant(true)]
[assembly:
AssemblyVersion("1.0.0.0")]
[assembly:
AssemblyFileVersion("1.0.0.0")]

8. The main compilation
This bit is real easy, thanks to the fact that solution and project files are build scripts themselves:

<Target Name="Build">
   <
Message Text="Build solution for build $(CCNetLabel)" />
   <
MSBuild
      Projects="@(SolutionFile)"
      Targets="Build">
         <
Output
            TaskParameter="TargetOutputs"
            ItemName="AssembliesBuiltByChildProjects" />
   </
MSBuild>
</
Target>

9. NCover + Nunit to run unit tests with coverage analysis
Yet more third-party code, this time from http://www.kiwidude.com/blog/2006/07/nant-and-msbuild-tasks-for-ncover.html.  This code also gives us the functionality for step 10 below.  A UsingTask element is needed as follows:

<UsingTask
   
TaskName="MSBuild.NCoverExplorer.Tasks.NCover"
   
AssemblyFile="Tools\NCover\MSBuild.NCoverExplorer.Tasks.dll" />

And then we can actually perform our unit tests, as follows:

<Message Text="Running unit tests with code coverage against @(SolutionFile)" />
<
NCover ToolPath="Tools\NCover\"
    CommandLineExe="Tools\NUnit\nunit-console.exe"
    CommandLineArgs="@(SolutionFile) /xml=D:\Projects\MyProject\Build\SmokeTest\NUnit.xml /labels /nologo"
    CoverageFile="$(CoverageFile)"
    />

10. NCoverExplorer rolled up coverage report
The output from NCover consists of:

  1. A list of code statements that weren't executed
  2. An overall percentage of code that wasn't executed

While these are very useful, it's often nice to get something in between.  A great example is the NCoverExplorer report produced by kiwidude, which is described at http://www.kiwidude.com/blog/2006/07/ncoverexplorer-v134.html.  This one:

Rolled up code coverage report 

To get this, we have already downloaded the code we need (in step 9 above).  So can add in the following UsingTask and then call it as follows:

<UsingTask
   
TaskName="MSBuild.NCoverExplorer.Tasks.NCoverExplorer"
   
AssemblyFile="Tools\NCover\MSBuild.NCoverExplorer.Tasks.dll" />
<NCoverExplorer ProjectName="MyProject" 
    ToolPath="D:\Projects\MyProject\Build\Tools\NCoverExplorer-1.3.4"
    ReportType="4"
    OutputDir="$(ArtefactDirectory)"
    XmlReportName="$(ArtefactDirectory)\CoverageSummary.xml"
    HtmlReportName="$(ArtefactDirectory)\CoverageSummary.html"
    MergeFileName="$(ArtefactDirectory)\CoverageMerge.xml"
    ShowExcluded="True"
    SatisfactoryCoverage="75"
    FailMinimum="False"
    CoverageFiles="$(CoverageFile)"
    Exclusions="Assembly=*.Tests;Namespace=*.Tests*"
    />

You may notice that we're being lenient and not failing the build for a poor code coverage.  I have some philosophical issues about the use of code coverage as enforcement tool - i.e. I think developers should write unit tests to properly execute their code, not just to hit some or other coverage target.

11. FxCop for static code analysis
This step in the process ought to be fairly straightforward.  After all, the MSBuild Community Tasks Project that we downloaded for step 6 from http://msbuildtasks.tigris.org/ includes an FxCop task which looks like it does exactly what we want - run FxCop against our built binaries and save the results into an XML file.  Fabulous, except that the standard FxCop rulesbase is pretty onerous and includes some rules which (a) don't apply to our situation; or (b) we don't feel like satisfying.

Now the authors of FxCop realised this and so all the executables for FxCop include the ability to exclude certain rules.  But the authors of the MSBuild Community Tasks haven't quite gotten this far yet.  So we modified the code and rebuilt the library.  And in the spirit of open source, we submitted our change back to the project.  You can find it at http://msbuildtasks.tigris.org/issues/show_bug.cgi?id=19.

Once we had the revised binaries available, we setup our build process to look like this:

<FxCop 
    TargetAssemblies="@(FxcopTargets)"
    RuleLibraries="@(FxCopRuleAssemblies)" 
    AnalysisReportFileName="D:\Projects\MyProject\Build\SmokeTest\fxcop.xml"
    DependencyDirectories="$(MSBuildCommunityTasksPath);C:\Program Files\Extreme Optimization\Statistics Library for .NET\bin;D:\Projects\MyProject\Build\Tools\log4net-1.2.10\bin\net\2.0\debug;C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0"
    FailOnError="False"
    ApplyOutXsl="False"
    OutputXslFileName="C:\Program Files\Microsoft FxCop 1.35\Xml\FxCopReport.xsl"
    Verbose="False"
    includeSummaryReport="True"
    ExcludeRules="Microsoft.Design#CA2210;Microsoft.Globalization#CA1303"
   
/>

You may notice that again we're being fairly lenient here - an FxCop failure will not break the build.  This is not the situation we want to be in, but our development work had started before we got FxCop into the process.  Consequently, there's this large chunk of code with a number of FxCop violations - we are in the process of working through this list and correcting the issues, but it's a slow process because it's being done in between the main dev tasks.

12. Putting the binaries in the drop folder
All the binaries from successful builds are captured and stored in a drop folder.  This is actually how we get code to our testers - we tell them to pick up build xxx where they will find such-and-such feature.

Copying the binaries to the drop location is really straightforward:

<Message Text="Copying files to $(ReleaseFolder)\v$(CCNetLabel)" />
<
Copy SourceFiles="@(ReleaseFiles)"
   
DestinationFolder="$(ReleaseFolder)\v$(CCNetLabel)" />

13. Merging XML files back into the CC.Net build report
This is made quite easy by CC.Net itself.  All you have to do add the following to the <publishers> section of your ccnet.config file and ensure that the correct XSL files are available within your web dashboard.

<merge>
    <
files>
        <
file>D:\Projects\MyProject\Build\SmokeTest\Nunit.xml</file>
        <
file>D:\Projects\MyProject\Build\SmokeTest\CoverageMerge.xml</file>
        <
file>D:\Projects\MyProject\Build\SmokeTest\CoverageSummary.xml</file>
        <
file>D:\Projects\MyProject\Build\SmokeTest\FXCop.xml</file>
    </
files>
</
merge>

Other changes we made to CC.Net

Replacement and tweaking of fxcop.xsl stylesheet:

The FxCop.xsl stylesheet that ships with CC.Net gives a pretty hard-to-read FxCop report.  We used the alternative stylesheet linked from http://confluence.public.thoughtworks.org/display/CCNETCOMM/XSL+Transforms.

We still weren't entirely happy with this stylesheet since it doesn't do any indentation of results, i.e. as you drill-down into your assembly all the FxCop messages are left-aligned without any additional indentation.  So we modified the stylesheet.  Revised version is attached to this post (with a slightly mangled name, thanks to our blog software).

Reconfiguring the web dashboard:

We pulled all the NAnt and Simian stuff out of the CC.Net dashboard.config file, since it's not relevant to our build.  We renamed the standard "NCover Output" to be "NCover Detail", since it is useful sometimes to find out which specific line number aren't being executed.  We then slotted in "NCover Overview" to be the NCoverExplorer rolled-up summary.

August 24 2006

Pingbacks and trackbacks (1)+

Comments are closed