ILearnable .Net

February 21, 2010

Buildsetup for fairly large SOA project: ccnet, nant, msbuild, svn… oh my!

Filed under: Uncategorized — andreakn @ 10:20

For the last 2 years I’ve been working on a Project which grew from the measly start of one project in one solution to 444 projects in 46 solutions. We have five environments we deploy to (in addition to the dev machines): trunk-build (1 server), hotfix-build (1 server), devtest (4 servers), test (13 servers) and production (17 servers). Of course all the servers doesn’t get set up with all the stuff, so there’s a fair bit of coordination and automation going on to make this work.

What I’m aiming to do here is to explain how our build process is setup, ending with how the code is deployed locally to the build server
in my next blog posting I’ll take it from there and explain how we deploy from one measly build server and drive the code, database schemas and configuration through the different environments ending up in the production environment.

First of all, a bit about the project: We’re building a SOA stack (WCF services) which encapsulates a lot of backend systems. Some of the backend systems are: customer repository, business rules systems, accounting system, information import- and export systems. There is a lot of scheduled tasks which do stuff like move binary files around (needed by some backend systems not written in this millenia), clean up data, charge recurring fees, etc. We also built a website for end customers which sits atop the SOA stack. (built with EPiServer/Asp.Net)

a small disclaimer: the word “project” is a very loaded word, and I’ll be using this word to refer to
1) the development effort, on its second year now
2) a .Net visual studio project (.csproj file)
3) a cruisecontrol.net project automating the build of (usually) one or (sometimes) more .Net solutions (.sln files)
which meaning is intended should be clear from the context. try to keep up 🙂

Build process

For our build process we use CruiseControl.Net, which is an awesome tool, but not exactly intuitive regarding best practices.
the root ccnet file looks approx like this:

<?xml version="1.0"?>
<!DOCTYPE projects [
  <!ENTITY Utilities SYSTEM "file:./CCNET_Projects/Utilities.xml">
  <!ENTITY WebSite SYSTEM "file:./CCNET_Projects/WebSite.xml">
  <!ENTITY ProtocolService SYSTEM "file:./CCNET_Projects/ProtocolService.xml">
  <!ENTITY ServiceRegistry SYSTEM "file:./CCNET_Projects/ServiceRegistry.xml">
  <!ENTITY SharedContracts SYSTEM "file:./CCNET_Projects/SharedContracts.xml">
  <!ENTITY SystemInfo SYSTEM "file:./CCNET_Projects/SystemInfo.xml">
  <!ENTITY CustomerServices SYSTEM "file:./CCNET_Projects/CustomerServices.xml">
  <!ENTITY AccountServices SYSTEM "file:./CCNET_Projects/AccountServices.xml">

  <!ENTITY Configuration "Release">
  <!ENTITY SvnRoot "svn://projects/soastack/code/trunk">
  ...
]>

<cruisecontrol>
  &WebSite;
  &Utilities;
  &ProtocolService;
  &ServiceRegistry;
  &SharedContracts;
  &SystemInfo;
  &CustomerServices;
  &AccountService;
  ...
</cruisecontrol>

so that each project is defined in a separate xml file located in c:\Program Files\CruiseControl.NET\server\CCNET_Projects. This keeps the ccnet.config file itself relatively small and easy to control. The only downside to using XML entities is that whenever a change occurs in one of the project files, the ccnet.config file needs to be touched for ccnet to pick up on the config change.

As you can see we define two xml text entities: &Configuration; and &SvnRoot; in the ccnet.config file. these two entities are used within all the project files (which we’ll come to in a bit) and is defined in the root config file for it to be easy to switch build target (Debug / Release) And SvnRoot (for the hotfix buildserver, which typically builds a branch: last last deployed to production).

Now, each of the project config files are practically identical, so much so that we have a template we use when creating new ccnet projects called Template.xml:

<!-- ******************************************** -->
<!-- ************ XXXXX ************           -->
<!-- ******************************************** -->
<!-- ***** When making changes in this file ***** -->
<!-- ***** YOU MUST ALSO TOUCH ccnet.config ***** -->
<!-- ***** for ccnet to reload new config ! ***** -->
<!-- ******************************************** -->

<project name="XXXXX"  queue="mainqueue">

  <modificationDelaySeconds>0</modificationDelaySeconds>
  <artifactDirectory>D:\BUILD\CCNETArtifacts\XXXXX</artifactDirectory>
  <workingDirectory>D:\BUILD\CCNETWD\soastack\code\trunk\services\XXXXX\trunk</workingDirectory>
  <webURL>http://trunkbuildserver</webURL>

  <sourcecontrol type="svn">
    <trunkUrl>&SvnRoot;/services/XXXXX/trunk</trunkUrl>
    <workingDirectory>D:\BUILD\CCNETWD\soastack\code\trunk\services\XXXXX\trunk</workingDirectory>
    <executable>C:\Program Files\CollabNet Subversion\svn.exe</executable>
    <username>build</username>
    <password>svn</password>
  </sourcecontrol>
  <triggers>
    <intervalTrigger seconds="300" buildCondition="IfModificationExists"/>
  </triggers>

  <prebuild>
    <nant>
      <executable>C:\Program Files\nant\bin\nant.exe</executable>
      <nologo>true</nologo>
      <buildFile>C:\Program Files\CruiseControl.NET\server\CCNET_Projects\nant_prebuild.build</buildFile>
      <logger>NAnt.Core.XmlLogger</logger>
      <buildArgs>-D:SvnRoot=&SvnRoot;</buildArgs>
      <buildTimeoutSeconds>6000</buildTimeoutSeconds>
    </nant>
  </prebuild>

  <tasks>
    <nant>
      <executable>C:\Program Files\nant\bin\nant.exe</executable>
      <nologo>true</nologo>
      <buildFile>C:\Program Files\CruiseControl.NET\server\CCNET_Projects\base_nant_file.build</buildFile>
      <logger>NAnt.Core.XmlLogger</logger>
      <buildArgs>-D:CCNetConfig=&Configuration;</buildArgs>
      <targetList>
        <target>copyConfig</target>
      </targetList>
      <buildTimeoutSeconds>6000</buildTimeoutSeconds>
    </nant>

    <msbuild>
      <executable>C:\Windows\Microsoft.NET\Framework\v3.5\MSBuild.exe</executable>
      <workingDirectory>D:\BUILD\CCNETWD\soastack\code\trunk\services\XXXXX\trunk\Solution\</workingDirectory>
      <projectFile>Soastack.Service.XXXXX.sln</projectFile>
      <buildArgs> /noconsolelogger /p:Configuration=&Configuration; /v:q</buildArgs>
      <targets>Build</targets>
      <logger>ThoughtWorks.CruiseControl.MsBuild.XmlLogger,C:\Program Files\CruiseControl.NET\server\ThoughtWorks.CruiseControl.MsBuild.dll</logger>
      <timeout>300</timeout>
    </msbuild>

    <nant>
      <executable>C:\Program Files\nant\bin\nant.exe</executable>
      <nologo>true</nologo>
      <buildFile>C:\Program Files\CruiseControl.NET\server\CCNET_Projects\base_nant_file.build</buildFile>
      <logger>NAnt.Core.XmlLogger</logger>
      <buildArgs>-D:CCNetConfig=&Configuration;</buildArgs>
      <targetList>
        <target>performCI</target>
      </targetList>
      <buildTimeoutSeconds>6000</buildTimeoutSeconds>
    </nant>

  </tasks>
  <publishers>
    <merge>
      <files>
        <file>D:\BUILD\CCNETArtifacts\XXXXX\_msbuild.xml</file>
        <file>D:\BUILD\CCNETWD\soastack\code\trunk\services\XXXXX\trunk\Solution\_CoverageReport.xml</file>
        <file>D:\BUILD\CCNETWD\soastack\code\trunk\services\XXXXX\trunk\Solution\_nunit_*.xml</file>
        <file>D:\BUILD\CCNETWD\soastack\code\trunk\services\XXXXX\trunk\Solution\_ncover_*.xml</file>
        <file>D:\BUILD\CCNETWD\soastack\code\trunk\services\XXXXX\trunk\Solution\_fxcop_*.xml</file>
        <file>D:\BUILD\CCNETWD\soastack\code\trunk\services\XXXXX\trunk\Solution\_sm_*.xml</file>
      </files>
    </merge>
    <xmllogger />
  </publishers>
  <state type="state" directory="D:\BUILD\CCNETStates"/>
  <labeller type="dateLabeller"/>
</project>

All one does is to make a new copy of the file and do a global rename of XXXXX to whatever the solution is called which is to be built.
I’ll highlight some of the features in the project file:
– It is set up to check every 5 minutes if there are any changes checked into svn and kickstart a build if that is the case
– when a build is started it does the following:

  1. performs the prebuild-task (cleans up shared resources between ccnet projects)
  2. performs SVN update
  3. performs nant task “copyConfigs”  (in nant file on buildserver c:\Program Files\CruiseControl.NET\server\CCNET_Projects\base_nant_file.build
  4. Builds the solution  using MSBuild
  5. performs nant task “performCI” (in the same nant file as in 3)

let’s look at these in more detail:

1) nant-prebuild
The nant_prebuild.build file cleans a folder called “sharedAssemblies” in which each solution build dumps the readymade artifacts to be shared (mostly web service contract dlls).
(this, by the way is pretty much the only thing we don’t like with our setup, that compiled contract dlls are checked into svn and also built by the build server. We’ll probably change it soon, but it hasn’t given us too much grief, so I guess it’s a valid option)

here it is:

<?xml version="1.0"?>
<project name="FullSolution" default="performCI">

 <property name="Nant-contrib-dir" value="C:\Program Files\nantcontrib-0.85" overwrite="true" />
 <property name="project-name" value = "${CCNetProject}" />

 <property name="clean.pattern.bin" value="**/bin/**/*" />
 <property name="clean.pattern.obj" value="**/obj/**/*" />
 <property name="clean.pattern.xml" value="_*.xml" />

 <target name="*">
	<call target="clean_shared"/>
	<call target="fetch_shared"/>
	<call target="clean_solution" /> 
 </target>


 <target name="clean_shared">
	<delete>
		<fileset>
			<include name="D:/BUILD/CCNETWorkingDirectories/PATH/TO/THE/SOLUTION/FOLDER/**/*" />
		</fileset>
	</delete>
 </target>


 <target name="fetch_shared">
	<loadtasks assembly="${Nant-contrib-dir}\bin\NAnt.Contrib.Tasks.dll" />
	
	<svn-checkout 	uri="svn://path/to/svn/SharedAssemblies"
			destination="D:/PATH/TO/THE/SharedAssemblies"
			username="buildserver"
			password="someSVNpassword" />
 </target>


  <target name="clean_solution" >
    <delete failonerror="false" >     
      <fileset>
        <include name="${CCNetWorkingDirectory}/**/*.dll" />
        <include name="${CCNetWorkingDirectory}/**/*.exe" />
        <include name="${CCNetWorkingDirectory}/**/*.config" />
        <include name="${CCNetWorkingDirectory}/**/*.xml" />
      </fileset>
    </delete>
  </target>


</project> 

2) svn-update
nothing to see here, move along

3) “copyConfigs” task
when we check in our code in svn we don’t check in the config files usually. this is because each dev probably needs his/her own settings, and the buildserver needs its own specific settings. so what we do instead is we check in a shadow file called for instance web.config.build which is gotten from the svn update in 2) and copied by this target to web.config (which incidentally is needed by the csproj projects in order for them to build)

4) build the solution
Not much to see here, although there are some interesting pre and postbuild events. There is a prebuild event on all the projects in a solution which creates the assemblyinfo.cs file (not checked into svn) before the solution is built so that we can control which version number is put into the compiled dlls. We use the svn revision number on the solution folder as the last version number (ignored by .Net) and the svn revision number of the root folder (the soastack-folder) as the second last number (also ignored by .Net) which is useful in tracking which version is in which environment and creating hotfix-branches for them if needed. The first two numbers in the version number (which .Net do care about) we put in a .txt file next to the solution so we can control when we want to rev the version.

the command line in the prebuild event is thusly:
nant -buildfile:"$(SolutionDir)assemblyInfoGen.build" -D:SolutionDir="$(SolutionDir)\" -D:ProjectDir="$(ProjectDir)\" -D:"AssemblyName=$(TargetName)"
and the assemblyInfoGen.build file which lies next to each solution is like this:

<!-- 
EXPECTED FILES:
===============
The following files must exist in the SolutionDir:

"assemblyInfoGen.build"        (This script)

"versionnumber.txt"            Only containing the first three parts of the assembly version number, like: 1.2.0
                               If this file exists in the project directory (ProjectDir) it will be used instead 
                               of the one the SolutionDir. 

"assemblyInfoGen-dont-run.txt" (OPTIONAL) To speed up compilation time when developing, 
                               create a file named "assemblyInfoGen-dont-run.txt".
                               If this file exists and contains the text "true", then 
                               this script skips generating the AssemblyInfo.cs file.
                               THIS FILE MUST NOT BE ADDED TO SVN (because this might 
                               lead to errors when running automated builds)

PREREQUISITS:
=============
1) NAnt must be installed, and full path to nant.exe must be added to the PATH environment variable
   More info about NAnt: http://nant.sourceforge.net/

2) TortoiseSVN must be installed in the dir: %programfiles%\TortoiseSVN\bin\
   The script uses the %programfiles%\TortoiseSVN\bin\SubWCRev.exe (part of TortoiseSVN) to retrieve the SVN revision number
   More info about TortoiseSVN: http://tortoisesvn.tigris.org/

3) Add the "AssemblyInfo.cs" files to the SVN ignore list, or it will lead conflicts during automated build and regular checkouts.

4) ADD THIS LINE TO THE PRE BUILD EVENT IN THE VISUAL STUDIO PROJECT:
   nant -buildfile:"$(SolutionDir)assemblyInfoGen.build" -D:SolutionDir="$(SolutionDir)\" -D:ProjectDir="$(ProjectDir)\" -D:"AssemblyName=$(TargetName)"

   INPUT PARAMETERS TO THIS NANT SCRIPT:
      SolutionDir	 - Full path to the solution dir (endring with "\") (VS: $(SolutionDir))
      ProjctDir    - Full path to the project dir (endring with "\") (VS: $(ProjectDir))
      AssemblyName - Name of the assembly (with no extension, like ".dll") (VS: $(TargetName))

AFTER FIRST RUN:
================
The script generates a "build_log.txt" file. This must be added the SVN ignore list
-->

<project name="" default="ConditionalRun">
  <target name="ConditionalRun">
    <property name="CondRunFile" value="${SolutionDir}/assemblyInfoGen-dont-run.txt" />
    <property name="DontRunAssemblyGen" value="false" />
    <loadfile file="${CondRunFile}" property="DontRunAssemblyGen" if="${file::exists(CondRunFile)}"/>
    <echo message="DontRunAssemblyGen=${DontRunAssemblyGen}" />
    <echo message="IS NOT RUNNING AssemblyInfoWithVersionNumberGen!" if="${string::contains(DontRunAssemblyGen,'true')}" />
    <call target="AssemblyInfoWithVersionNumberGen" unless="${string::contains(DontRunAssemblyGen,'true')}" />
  </target>

  <target name="AssemblyInfoWithVersionNumberGen">
    <echo message="SolutionDir: ${SolutionDir}" />
    <echo message="ProjectDir ${ProjectDir}" />
    <echo message="AssemblyName ${AssemblyName}" />

    <property name="LogFilePath" value="${SolutionDir}build_log.txt" />
    <property name="ProgramsDir" value="${environment::get-variable('programfiles')}"/>
    <property name="SubWCRevPath" value="${ProgramsDir}\TortoiseSVN\bin\SubWCRev.exe"/>
    <property name="CurrentYear" value="${int::to-string(datetime::get-year(datetime::now()))}"/>
    <property name="VersionNumberFileName" value="versionnumber.txt" />

    <call target="GetRevisionNumbers" />
    <property name="VersionNumberFilePath" value="${SolutionDir}${VersionNumberFileName}"/>
    <property name="VersionNumberFileFileExists" value="${file::exists(VersionNumberFilePath)}" />
    <property name="ProjVersionFilePath" value="${ProjectDir}${VersionNumberFileName}" />
    <property name="ProjVersionFileExists" value="${file::exists(ProjVersionFilePath)}" />

    <if test="${not VersionNumberFileFileExists}">
      <if test="${not ProjVersionFileExists}">
        <fail message="No ${VersionNumberFileName} file was found at either the solution or project level. Excpected to find the file in one of the following paths:
              '${ProjVersionFilePath}' or '${VersionNumberFilePath}'"/>
      </if>
    </if>

    <if test="${ProjVersionFileExists}">
      <echo message="Using project specific version number file!"/>
      <property name="VersionNumberFilePath" value="${ProjVersionFilePath}"/>
    </if>

    <echo message="Version number file: '${VersionNumberFilePath}'"/>

    <!-- Load first part of version number string (x.x) from the version number file -->
    <loadfile file="${VersionNumberFilePath}" property="VersionNumber"/>
    <!-- Add . at the end if it does not exist -->
    <if test="${not string::ends-with(VersionNumber, '.')}">
      <property name="VersionNumber" value="${VersionNumber}." />
    </if>
    <property name="NewVersionNumber" value="${VersionNumber}${RootRevNumber}.${RevNumber}"/>
    <property name="TestAssemblyName" value="${AssemblyName}.Test" />
    <property name="TestAssemblyPath" value="${SolutionDir}${TestAssemblyName}" />
    <property name="AssemblyInfoPath" value="${ProjectDir}Properties\AssemblyInfo.cs" />

    <echo message="NewVersionNumber: ${NewVersionNumber}" />
    <echo message="Generating: ${AssemblyInfoPath}" />
    <asminfo output="${AssemblyInfoPath}" language="CSharp">
      <imports>
        <import namespace="System.Reflection"/>
      </imports>
      <attributes>
        <attribute type="AssemblyVersionAttribute" value="${VersionNumber}0.0"/>
        <attribute type="AssemblyFileVersionAttribute" value="${NewVersionNumber}"/>
        <attribute type="AssemblyTitleAttribute" value="${AssemblyName}"/>
        <attribute type="AssemblyDescriptionAttribute" value=""/>
        <attribute type="AssemblyCopyrightAttribute" value="Copyright (c) ${CurrentYear}."/>
      </attributes>
    </asminfo>

    <echo file="${LogFilePath}" append="true"
			message="${datetime::to-string(datetime::now())}	${NewVersionNumber}	${AssemblyName}	(${AssemblyInfoPath})" />

  </target>

  <target name="GetRevisionNumbers">
    <property name="RevNumber" value="0" />
    <property name="RootRevNumber" value="0" />

    <exec program="svn"
        workingdir="${ProjectDir}"
        commandline='info --xml'
        output="svninfo.xml"
        failonerror="false"/>

    <xmlpeek
        file="svninfo.xml"
        xpath="/info/entry/commit/@revision"
        property="RevNumber"
        failonerror="false"/>

    <xmlpeek
        file="svninfo.xml"
        xpath="/info/entry/@revision"
        property="RootRevNumber"
        failonerror="false"/>

    <echo message="Got revision numbers. Root: ${RootRevNumber} Project: ${RevNumber}"/>

    <delete file="svninfo.xml" failonerror="false" />
  </target>
</project>

We also have post build events which copies contract dlls into a sharedassemblies folder (from where they are referenced by other .net projects)

5) “performCI” nant task.
this task is responsible for performing unit tests and deploying the project to the build server environment (on the same server)

It is split into two, there is the base_nant_file.build which resides on the build server which is basically just a passthrough to the real script (there is a separate one for each .net solution). Here is the base_nant_file.build:

<?xml version="1.0"?>
<project name="FullSolution" default="performCI">

  <target name="doNothing">

  </target>	

  <target name="performCI">
    <nant buildfile="${CCNetWorkingDirectory}\Solution\default.build" target="performCI">
      <properties>

        <property name="nunit-console" value="C:\Program Files\NUnit\bin\nunit-console.exe" />
        <property name="ncover-console" value="C:\Program Files\NCover\NCover.Console.exe" />
        <property name="ncoverexplorer-console" value="C:\Program Files\NCover\NCoverExplorer\NCoverExplorer.Console.exe" />
        <property name="fxcop-console" value="C:\Program Files\Microsoft FxCop 1.35\FxCopCmd.exe" />
        <property name="nunit-location" value="C:\Program Files\NUnit\bin" />
        <property name="sourcemonitor.executable" value="C:\Program Files\SourceMonitor\SourceMonitor.exe" />
        <property name="xsl-dir" value="C:\Program Files\CruiseControl.NET\server\xsl" />
        <property name="Nant-contrib-dir" value="C:\Program Files\nantcontrib-0.85" overwrite="true" />

        <property name="configuration" value="${CCNetConfig}" />
        <property name="project-name" value = "${CCNetProject}" />
        <property name="deploy-dir" value="d:\Deploy\${CCNetProject}" />
        <property name="solution-dir" value="${CCNetWorkingDirectory}\Solution" />
        <property name="temp-dir" value="d:\BUILD\TEMP\${CCNetProject}" />

      </properties>
    </nant>
  </target>


  <target name="copyConfig">
    <nant buildfile="${CCNetWorkingDirectory}\Solution\default.build" target="copyConfig">
      <properties>
        <property name="nunit-console" value="C:\Program Files\NUnit\bin\nunit-console.exe" />
        <property name="ncover-console" value="C:\Program Files\NCover\NCover.Console.exe" />
        <property name="ncoverexplorer-console" value="C:\Program Files\NCover\NCoverExplorer\NCoverExplorer.Console.exe" />
        <property name="fxcop-console" value="C:\Program Files\Microsoft FxCop 1.35\FxCopCmd.exe" />
        <property name="nunit-location" value="C:\Program Files\NUnit\bin" />
        <property name="sourcemonitor.executable" value="C:\Program Files\SourceMonitor\SourceMonitor.exe" />
        <property name="xsl-dir" value="C:\Program Files\CruiseControl.NET\server\xsl" />
        <property name="Nant-contrib-dir" value="C:\Program Files\nantcontrib-0.85" overwrite="true" />


        <property name="configuration" value="${CCNetConfig}" />
        <property name="project-name" value = "${CCNetProject}" />
        <property name="deploy-dir" value="d:\Deploy\${CCNetProject}" />
        <property name="solution-dir" value="${CCNetWorkingDirectory}\Solution" />
        <property name="temp-dir" value="d:\BUILD\TEMP\${CCNetProject}" />
      </properties>
    </nant>
  </target>

</project>

as you can see the base_nant_script.build applies parameters which can be used by the *real* nant script which resides next to the .sln file in a nant file called default.build.
The reason why we did it this way is that now the default.build file is independent of the buildserver it runs on, and can be used with different build servers, each buildserver only needs to have its own base_nant_file containing the paths to resources. The default.build file is also independent of the naming conventions of cruisecontrol.net as the ccnet-parameters are translated into other parameters.

The default.build file for a typical project looks like this (I’ve anonymized it slightly)

<?xml version="1.0"?>
<project name="FullSolution" default="performCI" basedir=".">
  <property name="clean.pattern.xml" value="_*.xml" />
  <property name="ServiceProjectName" value="Service.AccountingService" />

  <target name="copyConfig">
    <echo message="copying configs" />
    <foreach item="File" property="filename">
      <in>
        <items>
          <include name="${ServiceProjectName}/*.config.build" />
        </items>
      </in>
      <do>
        <regex pattern="(?'configname'[^\\]+\.config)\.build$" input="${filename}" />
        <echo message="Original: ${filename}  New: ${configname}" />
        <copy file="${filename}" tofile="${ServiceProjectName}/${configname}" />
      </do>
    </foreach>
  </target>

  <target name="performCI" depends="clean" >
    <call target="unitTests"/>
    <call target="deploy" />
  </target>

  <target name="clean" description="remove generated files">
    <delete>
      <fileset>
        <include name="${clean.pattern.xml}"/>
      </fileset>
    </delete>
  </target>
  
  <target name="unitTests">
    <echo message="starting unittests" />
    <property name="testsfailed" value="0" />

    <foreach item="File" property="filename">
      <in>
        <items>
          <include name="**\bin\*\*test.dll"></include>
          <exclude name="**\bin\*\*externalintegrationtest.dll"></exclude>
        </items>
      </in>
      <do>
        <echo message="doing nunit on a file ${path::get-file-name-without-extension(filename)}" />
        <echo message="/xml:${project::get-base-directory()}\_nunit_${path::get-file-name-without-extension(filename)}.xml" />
        <echo message="${filename}" />
        <echo message="- - - - - - - -"/>
        <exec program="${nunit-console}" failonerror="false" resultproperty="testresult.temp">
          <arg value="${filename}" />
          <arg value="/xml:${project::get-base-directory()}\_nunit_${path::get-file-name-without-extension(filename)}.xml" />
        </exec>

        <if test="${int::parse(testresult.temp)!=0}">
          <property name="testsfailed" value="1" />
        </if>
      </do>
    </foreach>
    <fail message="Failures reported in unit tests." unless="${int::parse(testsfailed)==0}" />
  </target>

  <target name="deploy" >
    <mkdir dir="${deploy-dir}" if="${directory::exists(deploy-dir) == false}" />
    <delete>
      <fileset>
        <include name="${deploy-dir}/**/*.*" />
      </fileset>
    </delete>
    <copy todir="${deploy-dir}" flatten="true" >
      <fileset basedir="${solution-dir}">
        <include name="**/bin/*/*.svc" />
        <include name="**/Web.config" />
        <include name="**/log4net.config" />
      </fileset>
    </copy>

    <copy todir="${deploy-dir}\bin" flatten="true" >
      <fileset basedir="${solution-dir}">
        <include name="**/bin/*/*.exe" />
        <include name="**/bin/*/*.dll" />
        <include name="**/bin/*/*.xml" />
        <include name="**/bin/*/*.pdb" />
        <exclude name="**/bin/*/*Test.dll" />
        <exclude name="**/bin/*/*Stub.dll" />
      </fileset>
    </copy>

    <!-- Copy ProtocolService stub to bin directory so that external integration tests don't go to Back End -->
    <copy todir="${deploy-dir}\bin" file="${solution-dir}\..\..\..\SharedAssemblies\Lib\Stubs\Service.ProtocolServiceStub.dll" />
  </target>
</project>

so to explain what happens in a bit more detail here are the targets:
1) copyConfig
I’ve already explained what this does and why, but instead of hardcoding all the config file names to be copied we use the convention that any file named *.config.build should be copied into *.build (we use a regex to accomplish this). The reason why this target is duplicated into each and every default.build file is because some solutions have more intricate requirements for configs

2) clean
nothing to see here, move on

3) unitTests
This target performs the unit tests using nunit-console. We used to do all sorts of analysis in addition to performing unit tests (code coverage, static code analysis etc.) but found they were more a nuisance than actually helpful, and as they slowed the build down we took them out. Our Nunit tests are separated into three different kinds
a) unit tests (run on each build) typically tests within one project in a solution. mocks out everything else)
b) internal integration tests (tests the entire solution, mocks out any external references)
c) external integration tests (tests the entire SOA stack as a whole, only mocks out external backend systems which aren’t prepared for unit testing (for instance the accounting system which would throw a fit if we made a gazillion fake transactions regularly… actually, the system would handle it fine, but the people in the accounting division would probably throw a fit when the numbers didn’t match the “reality” of the test system which they own and operate) )

The last category of tests aren’t run during the build but are rather run regularly by a separate ccnet-project called “IntegrationTests” which runs unit tests which drive the SOA stack as deployed on the build machine

4) deploy
The deploy target deploys the code to a local folder on the build machine where it can be exersized by the external integration tests and is available for manual testing through the website which is also deployed in the same manner

Advertisements

4 Comments »

  1. Nice write-up! I’m in the process of defining the CI implementation for our environment and found this to be a great resource. You mentioned a second article, is that something that you are still planning on doing?

    Comment by Mike — April 28, 2010 @ 14:14 | Reply

    • Done, (sorry it took a while)

      Comment by andreakn — May 8, 2010 @ 20:22 | Reply

  2. Hi, I’m new to ccnet and nant and I try to understand your article. Could you add the “nant_prebuild.build” file to clarify the whole process?

    Comment by Thomas — September 24, 2010 @ 09:09 | Reply

    • I’ve added it now, sorry for the omission

      Comment by andreakn — September 24, 2010 @ 09:23 | Reply


RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Create a free website or blog at WordPress.com.

%d bloggers like this: