Saturday, January 24, 2009

Continuous Integration : 101

Continuous integration is a term with mixed meaning, usually intended to describe an automated build process associated with a development groups source control system. Typically on check-in the build server polls the source control repository, gets the latest source code and proceeds to build the code. Notifications can be on success or failure of the build meaning we can always have a very recent view of the health of the code base.

CI is a very helpful part of an agile teams delivery and can greatly aid in improving the build/deployment times and just as importantly can keep the quality of source code high. Unfortunately there seems to be a large number of agile teams that are not embracing this very simple and beneficial process; it seems there is a high perceived point of entry, so lets try to eliminate this; this whole process takes less than 30 minutes.

*NOTE: this is not going to be a ground breaking article. However if you have never set up a CI environment then this is, I am sure, a worthwhile read. It is also not a generalisation. This is just how I do it. Feel free to adapt and modify at will. This example is just using the set up I have for the contract I am on at the moment: VS2008, MS-Test, VSS, MS-Build, TeamCity... I am sure it is known that they are not my preferred tests, source control or build script... so use your initiative and figure out those specifics if yours are different... its really not very hard.

Step 1: Set up a Solution Build configuration

Assuming you have a solution already set up, open the solution in VS and right click on the solution and select the configuration manager.

img1

Set up a new solution configuration that inherits from debug:

img2



Name this new configuration AutomatedDebug and make sure you create a new solution configuration for it (the check box). As a note I prefer to get it to inherit from Debug.


image

Now set the build in VS to AutomatedDebug in the menu (the drop down usuallly next to the play/Debug button) should read AutomatedDebug)

Step 2: Configure your Automated Build

I believe part of having an easy to use solution is having everything required to run and build the solution in source control. I find the following file structure a good start

img4

In the Lib folder is all my references. I try to keep non framework things out of the GAC, it just leads to headaches. Basically anything external DLL XML or config that you do not control and is required to build goes here.

In the Tools folder is everything thing that is not code that runs, things like Code Gen tools, Build tools, Templates, Snippets, Static Analysis tools, Test runners. Some think this is over kill and is a waste of disk space. Disk space is cheap, my time is not. I want to be able to get latest and hit build and it should work.

In the Src is all the source code, basically your solution sits in here: Be sure to include SQL.. it is code that runs, so it belongs in source control.

I prefer to have all of my output in one build folder so I go through all of the projects properties and change the output for the AutomatedDebug build to ..\..\Build\AutomatedDebug\ with the XML docs in the same place. I turn on "Warnings to Errors" and Warning to level 4 plus I set FXCop to the desired settings (personally I don't care about localisation, mobility etc.).

My test projects are set up a little different, the difference here is I like my tests in a separate folder so have ..\..\Build\AutomatedDebugTest\ for my output for test projects with no increased warnings or FXCop or XML docs.

As anything in the Build folder is build output you do not need to put this in source control and its is perfectly ok to delete this folder at any time.

*NB: when adding a project to the solution the project specific version of that build configuration will not automatically be added, you have to create this yourself. Go into the solution config manager again and on the new project create a new config to match the others.

Step 3: Create a Build Script

This can be the tedious part of the process, so I am just going to give you one ready to go. Place the build script in your Tools folder as it is ineffect a tool. This build script is MSBuild as this is the build tool I get the least friction with in terms of management acceptance. Other options are Nant, Rake, PSake and Bake.

<Project DefaultTargets="Tests" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="3.5">
<!-- User defined properties-->
<!-- Anything that is a proper unit test goes in these projects (ie they run fast and do not hit concrete dependencies). Replace the project names below with the the correct names in your project. Add or removes lines as necessary -->
<ItemGroup>
<UnitTestProject Include="$(MsTestPrefix).UnitTestProject1.dll" />
<UnitTestProject Include="$(MsTestPrefix).UnitTestProject2.dll" />
<UnitTestProject Include="$(MsTestPrefix).UnitTestProject3.dll" />
<UnitTestProject Include="$(MsTestPrefix).UnitTestProject4.dll" />
</ItemGroup>
<!-- Anything that is an integration test goes in these projects (ie they run slow and hit concrete dependecies like file system or databases).Replace the project names below with the the correct names in your project. Add or removes lines as nessecary -->
<ItemGroup>
<IntegrationTestProject Include="$(MsTestPrefix).IntegrationTestsProject1.dll" />
<IntegrationTestProject Include="$(MsTestPrefix).IntegrationTestsProject2.dll" />
</ItemGroup>
<!-- Replace MySolutionName with the name of your solution and MyCompany with the name of you default namespace prefix (if any)-->
<PropertyGroup>
<SolutionFile>..\MySolutionName.sln</SolutionFile>
<OutputDirectory>..\Build</OutputDirectory>
<Configuration>AutomatedDebug</Configuration>
<TestOutputDirectory>$(OutputDirectory)\$(Configuration)Test</TestOutputDirectory>
<ReportOutputDirectory>$(OutputDirectory)\$(Configuration)Reports</ReportOutputDirectory>
<DefaultNamespace>MyCompany.MySolutionName</DefaultNamespace>
<MsTestPrefix>/testcontainer:$(TestOutputDirectory)\$(DefaultNamespace)</MsTestPrefix>
<UnitTests>@(UnitTestProject, ' ')</UnitTests>
<IntegrationTests>@(IntegrationTestProject, ' ')</IntegrationTests>
<!-- Gets the list ofthe integrations test ready to be put in a MST exe call-->
</PropertyGroup>
<!-- User targets -->
<Target Name="VS">
<Message Text="***********$(VS80COMNTOOLS)***********" />
<Message Text="***********$(VS90COMNTOOLS)***********" />
</Target>
<Target Name="Clean">
<RemoveDir Directories="$(OutputDirectory)"
Condition="Exists($(OutputDirectory))">
</RemoveDir>
</Target>
<Target Name="Build" DependsOnTargets="Clean">
<MSBuild Projects="$(SolutionFile)"
Properties="Configuration=$(Configuration)" >
</MSBuild>
</Target>
<!-- http://geekswithblogs.net/michaelstephenson/archive/2007/04/27/112031.aspx -->
<Target Name="AllTests" DependsOnTargets="UnitTests; IntegrationTests">
<RemoveDir Directories="$(ReportOutputDirectory)"
Condition="Exists($(ReportOutputDirectory))">
</RemoveDir>
</Target>
<Target Name="UnitTests" DependsOnTargets="Build">
<MakeDir Directories="$(ReportOutputDirectory)"/>
<Exec Command='"$(VS90COMNTOOLS)..\IDE\mstest.exe" $(UnitTests) /runconfig:..\LocalTestRun.testrunconfig /resultsfile:"$(ReportOutputDirectory)\UnitTestResults.trx"' />
</Target>
<Target Name="IntegrationTests" DependsOnTargets="Build">
<MakeDir Directories="$(ReportOutputDirectory)"/>
<Exec Command='"$(VS90COMNTOOLS)..\IDE\mstest.exe" $(IntegrationTests) /runconfig:..\LocalTestRun.testrunconfig /resultsfile:"$(ReportOutputDirectory)\IntegrationTestResults.trx"' />
</Target>
</Project>


Quick over view of what the build file does (skip this if you are comfortable with MSBuild/Nant)




  1. We define that this is an MSBuild XML file with a default target.


  2. We set up all the variables. The tags are not special tags, MSBuild Allows you to use self defined XML tags as the variable names. Note we can use other variables to define further variables. Of special note is the way the test project names are collated together. The "UnitTests" and "IntegrationTests" nodes in the "PropertyGroup" Node use MSBuild syntax to concatenates the project names previously specified with the given format so we can run MS Test later.


  3. We define a target. The first we have called "Clean" and it delete the build output folder. That's it.


  4. The "Build" target specifies our solution to build with the predefined build configuration, being "AutomatedDebug" as specified in the local variable section. Also note that the Build depends on the Clean target. This means "Clean" will always run before this target.


  5. We define a "UnitTest" and an "IntegrationTest" target that both run test and depend on Build and therefore Clean. These targets are very specific to MS Test, most of the other runners are much easier to set up. This also requires VS on the build machine. To do this without VS installed check out the Gallio project.



Be sure to add test project names in as they are added or your build server will not be running those tests! Create a Build.bat file in the toolls folder with your build script and insert the following text so you can run the script.



@C:\Windows\Microsoft.NET\Framework\v3.5\MSbuild.exe AutomatedDebug.build /t:AllTests /l:FileLogger,Microsoft.Build.Engine;logfile="AllTests.log"
@pause


This assumes your are using .Net 3.5 and the name of your build file is AutomatedDebug.Build which resides in the same folder. the last part gives a log file of the build output, which I find handy, although verbose.



I really try to stress using very slim bat files to call my build scripts. They should be one liners that call a target in the build script. Your targets are like methods, target should be very specific, do one thing and do it well...Like your very well written code ;) If you need to do lots of things then create a target that depends on the other target you need to run (see the build file)



Step 4: Set up Source Control



I am not going to tell you how to set up Source control, you should be able to this by yourself. I am currently using VSS *gasp* but this process works with SVN, TFS, GIT... the source control tool really doesn't matter in terms of this specific process. I do prefer the build server to have a read-only login to retrieve the code however.



One more reminder: DO NOT CHECK IN THE BUILD FOLDER! This is a throw away folder that should not be in source control.



Step 5: Set up your Build Server



TeamCity is my new favourite toy. In 7 minutes I had downloaded, installed and set up a build server. Awesome. CC.Net is the other big name in .Net CI, but the XML config has caused many a headache, TC was so easy I have to recommend it. Follow this video to get up and running. Typically this is done on a separate machine. It does not need to be a flash machine, in fact I would recommend whatever is lying around not getting used, mine is a 4 year old single core laptop.



Now to set up notifications. I personally like the tray notification tool, clean and out of the way.



Now is a good place to add in the extra tasty stuff to you build script like code analysis tools (Simian, NDepend, Code Coverage/NCover), Deployment (Click-Once, WIX packaging), Documentation (Sandcastle) etc. These things normally get left off the shopping list as they add to the build time. Well, now another machine is doing the build and it doesn't affect me. Be sure to add these in as separate targets in the script to keep things nice and clean.



Wrap up



Now that was pretty trivial wasn't it? To be honest the hardest thing is setting up the solution folder structure and build script. This set up has served me well for a while now, I'm sure there are many others doing things differently, but this is as good a place to start as any.



Key points are:




  • Have a separate build config that is set up for high code quality with a separate output folder for test projects


  • Keep a clean file system, preferably use a root build output folder for all build, test and reports (code coverage,test results, static analysis etc)


  • Keep build project targets focused


  • Use TeamCity; its the easiest part of the whole process, but is pointless if the rest is not set up ready to go.

2 comments:

Lee Campbell said...

Nice. Do you have any preferences for maintaining versions of configs? For example how do you manage the differences in dev/sit/uat/prod? Is this related to any of your build scripts?

RhysC said...

Hell yeah! This is very easily done with this approach. A build target that runs PRIOR to the build (ie between clean and build) can copy/rename configs. Classic example is Web.Config.dev gets copied to Web.config. Ideally you also have seperate build output folders for each environment (AutomatedDebug, AutomatedTest, AutomatedUat etc) where each script is built to. So you are not over writing stuff.

For more options check out Scott Hanselman really good post on pre build events.
http://www.hanselman.com/blog/ManagingMultipleConfigurationFileEnvironmentsWithPreBuildEvents.aspx
It not how i do it but if you are not using a build scrip then it may be an option (and also gave me the original idea to separate build scripts for each build type)