A new project I've been toying with at work is
CruiseControl.Net. CCNet is an open source continuous build integration server that is highly configurable out of the box and provides plenty of room for extension using their object model.
With minimal configuration I was able to set the server up to:
- Poll the CVS source repository for changes.
- Build our project using the already installed DevEnv. CCNet also supports Nant and MSBuild.
- Execute our unit tests with NUnit.
- Tag the successfully built files in CVS with an automated build number.
- Send out an email to our team listing the outcome of the build including the results of the unit tests and which files changed since the last build.
Using their architecture I was able to create a custom task to run after the source files were updated to change the version in the AssemblyInfo.cs to automated build version. I ran into a few 'gotchas' along the way developing this task and thought I'd share my experience and approach to this problem.
Task Requirements:
- Files are under CVS source control, so the task must interact with the source control to properly Update, Edit, Change, Commit, Unedit the file.
- AssemblyInfo.cs files should not be modified if the code on server is not currently compiling.
- The modifications to the assembly info classes should not be perceived as relevant changes to the CruiseControl server, thus creating a situation where builds are attempted with no pertinent source files being changed.
- Have the CCNet server include the assembly version in the results email.
Creating the Custom Task:
The developers of CCNet have provided an interface ITask that you must implement in order to successfully integrate your code with the CCNet server.
The only required method in the ITask interface is:
public void Run(IIntegrationResult result)
Simple enough, so we create a class AssemblyVersionTask and implement the ITask assembly.
NOTE: The output assembly must be named
ccnet.*.plugin
and be placed inside the
CCNet\server
directory for it to be found by the server.
The Execute method hands us an object that implements IIntegrationResult. This object contains all of the necessary information about the current build needed to perform a task, such as root directory of project we are building, build number that is in progress, current state of the build, and a collection of TaskResults that are used on the CCNet dashboard and in the results emails.
In my case, I needed two other external pieces of information to be able to complete my task, the path to the CVS client on the CCNet server, and the mask to generate the correct version for the assemblies. We are able to specify these values as part of our task's configuration in the ccnet.config.
Configuring the CruiseControl Server for our Task:
CCNet uses a project called NetReflector to dynamically load objects and populate configuration values. Below is what the configuration looks like for my task:
<assemblyversion>
<versionMask>1.0.0.*</versionMask>
<pathToCVS>c:\program files\cvsnt\cvs.exe</pathToCVS>
</assemblyversion>
In my class I use NetReflector attributes to bind this configuration to my task:
using System;
using System.Collections;
using System.Collections.Specialized;
using System.IO;
using System.Threading;
using Exortech.NetReflector;
using ThoughtWorks.CruiseControl.Core;
namespace ccnet.AssemblyVersionTask.plugin
{
[ReflectorType("assemblyversion")]
public class AssemblyVersionTask : ITask
{
[ReflectorProperty("versionMask", Required=true)]
public string VersionMask;
[ReflectorProperty("pathToCVS", Required=true)]
public string PathToCVS;
.....
The ReflectorType attribute ties the "assemblyversion" node to our class and ReflectorProperty populates our public members with the values from the configuration.
The high level view of the task's processing is as follows:
public void Run(IIntegrationResult result)
{
StringCollection assemblyInfoFiles = GetAssemblyInfoFiles();
ModifyFiles(assemblyInfoFiles);
result.MarkStartTime();
result.AddTaskResult("<AssemblyVersionResult>All output assemblies marked version " + versionToSet + "</AssemblyVersionResult>");
}
I get a collection of all AssemblyInfo.cs files in my project's tree and then modify them. Now it gets interesting.
Remember requirement 3, about not forcing us into a constant loop of builds because the changes my to AssemblyInfo.cs would register as modifications to our project? This is where
result.MarkStartTime()
comes in. This updates the start time of the current build to the current time such that when the next build is run it will look for changes that occurred after the previous start time. We are now assured that the only changes the server picks up as a modification are valid changes to the source, not changes to the AssemblyInfo.cs by our custom task.
result.AddTaskResult
adds a chunk of XML to the build log which we will later retrieve.
Configuring the Email Output:
Since we've added a node to the output <AssemblyVersionResult>, we can configure the server to include this information in the results email.
First we must create an XSL to define the the format of our message:
<?xml version="1.0"?>
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="html"/>
<xsl:variable name="resultsnode" select="//AssemblyVersionResult"/>
<xsl:template match="/">
<xsl:if test="count($resultsnode) > 0">
<br />
<table cellpadding="2" cellspacing="0" border="0" width="98%">
<tr>
<td class="sectionheader" colspan="2">
Assembly Versioning
</td>
</tr>
<tr>
<td><xsl:value-of select="$resultsnode"/></td>
</tr>
</table>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
The XSL file should be saved to the CCNet\server\xsl directory.
Then in the ccnet.exe.config the new XSL should be added to the list XSL files that are applied to the email results:
<xslFiles>
....
<file name="xsl\assemblyversion.xsl" />
....
</xslFiles>
All Done:
This guide serves as a high-level explanation on getting custom tasks configured and running within the CruiseControl.Net server. I glossed over the actual guts of my task as the setup of the task cause me more problems than the actual coding of the work the tasks performs.
Feel free to shoot me a message if you have any questions.
- jc