Setting up a Continuous Integration System, Part 9: Conclusion

September 22, 2009 13:27

This is the final part in an ongoing series about setting up a continuous integration system. The series includes:

  1. Part 1: Introduction
  2. Part 2: Project Folders
  3. Part 3: CI Workflow
  4. Part 4: CI Server Baseline Software
  5. Part 5: CruiseControl.NET Custom Plug-in: Update Version
  6. Part 6: CruiseControl.NET Custom Plug-in: Source Retrieval
  7. Part 7: Installing CruiseControl.NET and Custom Plug-ins
  8. Part 8: Configuring CruiseControl.NET
  9. Part 9: Conclusion (this post)

Setting up and maintaining—not to mention blogging about—a CI environment is a lot of work. It takes time, persistence, and a bit of dedication. Hopefully this series will give you a head start or at least a place to get more information on setting up a new CI environment or maintaining or even improving an existing one.

So, what's next?

Let's face it: no development project is ever truly done. The same goes for your CI environment. There's always a new process, tool, or procedure to add to make it just a little (or maybe a whole lot) better. I hear a lot of good things about NAnt. It's something I'd like to look into. Ditto for MSBuild scripting, which some people use to perform all of their CI tasks.

On the plug-in front, an immediate improvement to my updver (update version) plug-in would be if the plug-in accepted an array of assembly files to update rather than just the one per task declaration. The end result would be a cleaner and more readable ccnet.config file.

I'm sure there are other things. Perhaps some of them are future blog posts waiting to happen.

That's it… for now

This concludes this series of posts. I'll no doubt continue to edit the series as needed, keeping it updated with new ideas and that sort of thing. One of the charters of this blog is to serve as a documentation base for yours truly, so I know I'll be visiting the posts occasionally for refreshers and just to figure out how or why I did something the way I did it.


Setting up a Continuous Integration System, Part 8: Configuring CruiseControl.NET

September 21, 2009 07:18

This is the next part in an ongoing series about setting up a continuous integration system. The series includes:

  1. Part 1: Introduction
  2. Part 2: Project Folders
  3. Part 3: CI Workflow
  4. Part 4: CI Server Baseline Software
  5. Part 5: CruiseControl.NET Custom Plug-in: Update Version
  6. Part 6: CruiseControl.NET Custom Plug-in: Source Retrieval
  7. Part 7: Installing CruiseControl.NET and Custom Plug-ins
  8. Part 8: Configuring CruiseControl.NET (this post)
  9. Part 9: Conclusion

We're almost done. By this time you've got everything in place to embrace continuous integration. Everything but perhaps the most important part: the configuration of the CI server. Since I use CruiseControl.NET, I'll next focus on the heart of that configuration, the ccnet.config file.

Resources for Configuring Your CruiseControl.NET Server

Before I jump into the configuration of the config file, let me point out some resources for getting more information. First, there's ThoughtWorks' Configuring the Server online help. More specifically, there's the section on configuring the config file itself, which starts with the CruiseControl Configuration Block. From there, you can drill down into the information for each of the sub-blocks that live beneath <cruisecontrol>.

The Tree View perspective is another nice way of seeing the hierarchy of the ccnet.config file:

image

Editing the ccnet.config file

A quick word on editing the ccnet.config file: It's just a text file, of course, but it helps to use something other than a plain text editor (like notepad) to edit the file. Any editor with syntax highlighting is going to make the process a lot easier. I use Visual Studio or, more often, Notepad2.

The barebones ccnet.config file

The ccnet.config file lives in C:\Program Files\CruiseControl.NET\server. As installed, it's pretty barren:

   1: <cruisecontrol xmlns:cb="urn:ccnet.config.builder">
   2:     <!-- This is your CruiseControl.NET Server Configuration file. Add your projects below! -->
   3:     <!--
   4:         <project name="MyFirstProject" />
   5:     -->
   6: </cruisecontrol>

Not much happening there… yet. Let's add to the file step-by-step until we have a project that CruiseControl.NET can run through the entire CI process.

I'll show the full config file as I add each section to keep things in context.

1. Add a Project Configuration Block

The root tag pair is always <cruisecontrol>…</cruisecontrol>. From there, you add one or more <project>'s. I'll use my Silverlight TagCloud project as an example. Adding the <project> tag with accompanying attributes yields:

   1: <cruisecontrol>
   2:  
   3:     <!-- begin SilverlightTagCloud -->
   4:     <!-- online help: http://confluence.public.thoughtworks.org/display/CCNET/Project+Configuration+Block -->
   5:     <project name="SilverlightTagCloud" queue="SilverlightTagCloud" queuePriority="1">
   6:         <workingDirectory>C:\dev\solutions\SilverlightTagCloud</workingDirectory>
   7:         <artifactDirectory>C:\dev\solutions\SilverlightTagCloud\bin\ccnet\artifacts</artifactDirectory>
   8:         <webURL>http://server_name_or_ip/ccnet/server/local/project/SilverlightTagCloud/ViewLatestBuildReport.aspx</webURL>
   9:         <modificationDelaySeconds>20</modificationDelaySeconds>
  10:     </project>
  11:     <!-- end SilverlightTagCloud -->
  12:  
  13: </cruisecontrol>

I don't use every attribute possible (see the online documentation for a complete list), but I'll go over the ones I do use.

The <project> attributes I use are:

  • name: The name of the project. This is how it will appear through the CC.NET web interface as well as the cctray client app.
  • queue: CC.NET has this concept of integration queues, where by default each project will run in it's own queue (or thread). Alternatively, you could run, say, debug and release builds one after the other by labeling them with the same queue name. Now, technically I don't need this attribute defined, but I did when I used to have separate projects defined for each of debug and release builds. I leave it here now just in case I ever go back to that model.
  • queuePriority: This tells CC.NET the order in which to work on the projects in a queue. '1' indicates highest priority, with '0' denoting the project that will be worked on last after all others have been processed for that queue.
  • workingDirectory: The project's working folder. I always use absolute paths to avoid confusion.
  • artifactDirectory: The project's artifact folder where CC.NET task output is stored. Make sure this location is unique per project, otherwise things like NUnit, NCover, etc. sort of output will become mixed up. Again, I use absolute paths to keep things clear.
  • webUrl: The server where CC.NET runs. This is used by the cctray app so that when you double-click a project the web interface is brought up. If you're hosting on a web server accessible from the Internet you can check the status of your builds from anywhere.
  • modificationDelaySeconds: The minimum amount of seconds to wait after the last check-in is performed. For example, if the modification delay is set to 10 seconds and the last check-in was 7 seconds ago, the system will sleep for 3 seconds before doing anything.

2. Add a Trigger Block

   1: <cruisecontrol>
   2:  
   3:     <!-- begin SilverlightTagCloud -->
   4:     <!-- online help: http://confluence.public.thoughtworks.org/display/CCNET/Project+Configuration+Block -->
   5:     <project name="SilverlightTagCloud" queue="SilverlightTagCloud" queuePriority="1">
   6:         <workingDirectory>C:\dev\solutions\SilverlightTagCloud</workingDirectory>
   7:         <artifactDirectory>C:\dev\solutions\SilverlightTagCloud\bin\ccnet\artifacts</artifactDirectory>
   8:         <webURL>http://server_name_or_ip/ccnet/server/local/project/SilverlightTagCloud/ViewLatestBuildReport.aspx</webURL>
   9:         <modificationDelaySeconds>20</modificationDelaySeconds>
  10:         
  11:         <triggers>
  12:             <!-- triggers help: http://confluence.public.thoughtworks.org/display/CCNET/Interval+Trigger -->
  13:             <intervalTrigger name="continuous" seconds="300" buildCondition="IfModificationExists" />
  14:         </triggers>
  15:  
  16:     </project>
  17:     <!-- end SilverlightTagCloud -->
  18:  
  19: </cruisecontrol>

Triggers tell CC.NET when to fire up a new CI cycle. While there are numerous types, I tend to go with the Interval Trigger, which sets a time interval for CC.NET to wake up and do it's thing. It's a one line declaration, stuck between the <triggers>…</triggers> tags and, in the above example, uses the following attributes:

  • name: The name of the trigger.
  • seconds: The number of seconds to sleep between integration cycles. I tend to keep this high for my home CC.NET server because I'm not that concerned with having to wait a little while for a build to kick off. At work, I tend to keep this number at 60 seconds.
  • buildCondition: The condition used to determine when to kick off a CI cycle. The value 'IfModificationExists' indicates that a build will occur only when source modifications are detected. Our <sourcecontrol> block takes care of the actual determination.

3. Add a State Manager Block

   1: <cruisecontrol>
   2:  
   3:     <!-- begin SilverlightTagCloud -->
   4:     <!-- online help: http://confluence.public.thoughtworks.org/display/CCNET/Project+Configuration+Block -->
   5:     <project name="SilverlightTagCloud" queue="SilverlightTagCloud" queuePriority="1">
   6:         <workingDirectory>C:\dev\solutions\SilverlightTagCloud</workingDirectory>
   7:         <artifactDirectory>C:\dev\solutions\SilverlightTagCloud\bin\ccnet\artifacts</artifactDirectory>
   8:         <webURL>http://server_name_or_ip/ccnet/server/local/project/SilverlightTagCloud/ViewLatestBuildReport.aspx</webURL>
   9:         <modificationDelaySeconds>20</modificationDelaySeconds>
  10:         
  11:         <triggers>
  12:             <!-- triggers help: http://confluence.public.thoughtworks.org/display/CCNET/Interval+Trigger -->
  13:             <intervalTrigger name="continuous" seconds="300" buildCondition="IfModificationExists" />
  14:         </triggers>
  15:  
  16:         <state type="state" directory="C:\ccnetstate" />
  17:         
  18:     </project>
  19:     <!-- end SilverlightTagCloud -->
  20:  
  21: </cruisecontrol>

The State Manager Block tells CC.NET where to store such information as the build label, the time of the build, the outcome of the build, etc. It's a pretty simple block. I use the same location for all projects. It doesn't seem to cause any issues.

4. Add a Source Control Block

   1: <cruisecontrol>
   2:  
   3:     <!-- begin SilverlightTagCloud -->
   4:     <!-- online help: http://confluence.public.thoughtworks.org/display/CCNET/Project+Configuration+Block -->
   5:     <project name="SilverlightTagCloud" queue="SilverlightTagCloud" queuePriority="1">
   6:         <workingDirectory>C:\dev\solutions\SilverlightTagCloud</workingDirectory>
   7:         <artifactDirectory>C:\dev\solutions\SilverlightTagCloud\bin\ccnet\artifacts</artifactDirectory>
   8:         <webURL>http://server_name_or_ip/ccnet/server/local/project/SilverlightTagCloud/ViewLatestBuildReport.aspx</webURL>
   9:         <modificationDelaySeconds>20</modificationDelaySeconds>
  10:         
  11:         <triggers>
  12:             <!-- triggers help: http://confluence.public.thoughtworks.org/display/CCNET/Interval+Trigger -->
  13:             <intervalTrigger name="continuous" seconds="300" buildCondition="IfModificationExists" />
  14:         </triggers>
  15:  
  16:         <state type="state" directory="C:\ccnetstate" />
  17:         
  18:         <sourcecontrol type="svnget">
  19:             <executable>c:\program files\VisualSVN\bin\svn.exe</executable>
  20:             <workingDirectory>c:\dev\solutions\SilverlightTagCloud</workingDirectory>
  21:         </sourcecontrol>
  22:         
  23:     </project>
  24:     <!-- end SilverlightTagCloud -->
  25:  
  26: </cruisecontrol>

The Source Control Block defines from where CC.NET should pull source. My experience up to this point involves use of the blocks for Visual SourceSafe, Subversion, and Team Foundation Server. They all function more or less the same; source control is source control, from the perspective of CC.NET, anyway. In a previous post in this series I discussed my own custom source control plug-in for Subversion. I also explained why I use my own as opposed to using the stock one, so I won't go into that now. You should note that using one or the other is really not that dissimilar.

Briefly, the source control block's attributes:

  • type: The source control product to use.
  • executable: The location of the svn executable.
  • workingDirectory: The local project folder.

5. Add a Labeller Block

   1: <cruisecontrol>
   2:  
   3:     <!-- begin SilverlightTagCloud -->
   4:     <!-- online help: http://confluence.public.thoughtworks.org/display/CCNET/Project+Configuration+Block -->
   5:     <project name="SilverlightTagCloud" queue="SilverlightTagCloud" queuePriority="1">
   6:         <workingDirectory>C:\dev\solutions\SilverlightTagCloud</workingDirectory>
   7:         <artifactDirectory>C:\dev\solutions\SilverlightTagCloud\bin\ccnet\artifacts</artifactDirectory>
   8:         <webURL>http://server_name_or_ip/ccnet/server/local/project/SilverlightTagCloud/ViewLatestBuildReport.aspx</webURL>
   9:         <modificationDelaySeconds>20</modificationDelaySeconds>
  10:         
  11:         <triggers>
  12:             <!-- triggers help: http://confluence.public.thoughtworks.org/display/CCNET/Interval+Trigger -->
  13:             <intervalTrigger name="continuous" seconds="300" buildCondition="IfModificationExists" />
  14:         </triggers>
  15:  
  16:         <state type="state" directory="C:\ccnetstate" />
  17:         
  18:         <sourcecontrol type="svnget">
  19:             <executable>c:\program files\VisualSVN\bin\svn.exe</executable>
  20:             <workingDirectory>c:\dev\solutions\SilverlightTagCloud</workingDirectory>
  21:         </sourcecontrol>
  22:         
  23:         <labeller type="iterationlabeller">
  24:             <prefix>1.0</prefix>
  25:             <duration>1</duration>
  26:             <releaseStartDate>2009/8/15</releaseStartDate>
  27:             <separator>.</separator>
  28:         </labeller>
  29:         
  30:     </project>
  31:     <!-- end SilverlightTagCloud -->
  32:  
  33: </cruisecontrol>

The Labeller Block is used by CC.NET to generate build version labels. This label can then be used to version your assemblies. I like the Iteration Labeller, which takes the following attributes:

  • prefix: Any string to prepend to a label.
  • duration: The duration of the iteration in weeks; helps define how the label will be incremented.
  • releaseStartDate: The start date for iteration one. I typically use the date in which I added the project to CC.NET, just to start things off somewhere.
  • separator: The separator between iteration number and build number.

6. Add a Prebuild Block

   1: <cruisecontrol>
   2:  
   3:     <!-- begin SilverlightTagCloud -->
   4:     <!-- online help: http://confluence.public.thoughtworks.org/display/CCNET/Project+Configuration+Block -->
   5:     <project name="SilverlightTagCloud" queue="SilverlightTagCloud" queuePriority="1">
   6:         <workingDirectory>C:\dev\solutions\SilverlightTagCloud</workingDirectory>
   7:         <artifactDirectory>C:\dev\solutions\SilverlightTagCloud\bin\ccnet\artifacts</artifactDirectory>
   8:         <webURL>http://server_name_or_ip/ccnet/server/local/project/SilverlightTagCloud/ViewLatestBuildReport.aspx</webURL>
   9:         <modificationDelaySeconds>20</modificationDelaySeconds>
  10:         
  11:         <triggers>
  12:             <!-- triggers help: http://confluence.public.thoughtworks.org/display/CCNET/Interval+Trigger -->
  13:             <intervalTrigger name="continuous" seconds="300" buildCondition="IfModificationExists" />
  14:         </triggers>
  15:  
  16:         <state type="state" directory="C:\ccnetstate" />
  17:         
  18:         <sourcecontrol type="svnget">
  19:             <executable>c:\program files\VisualSVN\bin\svn.exe</executable>
  20:             <workingDirectory>c:\dev\solutions\SilverlightTagCloud</workingDirectory>
  21:         </sourcecontrol>
  22:         
  23:         <labeller type="iterationlabeller">
  24:             <prefix>1.0</prefix>
  25:             <duration>1</duration>
  26:             <releaseStartDate>2009/8/15</releaseStartDate>
  27:             <separator>.</separator>
  28:         </labeller>
  29:         
  30:         <prebuild>
  31:             <!-- assembly version update prior to build -->
  32:             <svnupdver>
  33:                <executable>c:\program files\VisualSVN\bin\svn.exe</executable>
  34:                <assemblyInfoFolder>C:\dev\solutions\SilverlightTagCloud\projects\TagCloudControl\Properties</assemblyInfoFolder>
  35:             </svnupdver>
  36:            <svnupdver>
  37:               <executable>c:\program files\VisualSVN\bin\svn.exe</executable>
  38:               <assemblyInfoFolder>C:\dev\solutions\SilverlightTagCloud\projects\TagCloudService\Properties</assemblyInfoFolder>
  39:            </svnupdver>
  40:         </prebuild>
  41:         
  42:     </project>
  43:     <!-- end SilverlightTagCloud -->
  44:  
  45: </cruisecontrol>

The Prebuild Block is a container for Task Blocks. I typically perform my assembly version update here via my own custom plug-in, though you can just as easily do it in the main Tasks Block.

7. Add a Task Block

   1: <cruisecontrol>
   2:  
   3:     <!-- begin SilverlightTagCloud -->
   4:     <!-- online help: http://confluence.public.thoughtworks.org/display/CCNET/Project+Configuration+Block -->
   5:     <project name="SilverlightTagCloud" queue="SilverlightTagCloud" queuePriority="1">
   6:         <workingDirectory>C:\dev\solutions\SilverlightTagCloud</workingDirectory>
   7:         <artifactDirectory>C:\dev\solutions\SilverlightTagCloud\bin\ccnet\artifacts</artifactDirectory>
   8:         <webURL>http://server_name_or_ip/ccnet/server/local/project/SilverlightTagCloud/ViewLatestBuildReport.aspx</webURL>
   9:         <modificationDelaySeconds>20</modificationDelaySeconds>
  10:         
  11:         <triggers>
  12:             <!-- triggers help: http://confluence.public.thoughtworks.org/display/CCNET/Interval+Trigger -->
  13:             <intervalTrigger name="continuous" seconds="300" buildCondition="IfModificationExists" />
  14:         </triggers>
  15:  
  16:         <state type="state" directory="C:\ccnetstate" />
  17:         
  18:         <sourcecontrol type="svnget">
  19:             <executable>c:\program files\VisualSVN\bin\svn.exe</executable>
  20:             <workingDirectory>c:\dev\solutions\SilverlightTagCloud</workingDirectory>
  21:         </sourcecontrol>
  22:         
  23:         <labeller type="iterationlabeller">
  24:             <prefix>1.0</prefix>
  25:             <duration>1</duration>
  26:             <releaseStartDate>2009/8/15</releaseStartDate>
  27:             <separator>.</separator>
  28:         </labeller>
  29:         
  30:         <prebuild>
  31:            <!-- assembly version update prior to build -->
  32:            <svnupdver>
  33:               <executable>c:\program files\VisualSVN\bin\svn.exe</executable>
  34:               <assemblyInfoFolder>C:\dev\solutions\SilverlightTagCloud\projects\TagCloudControl\Properties</assemblyInfoFolder>
  35:           </svnupdver>
  36:           <svnupdver>
  37:              <executable>c:\program files\VisualSVN\bin\svn.exe</executable>
  38:              <assemblyInfoFolder>C:\dev\solutions\SilverlightTagCloud\projects\TagCloudService\Properties</assemblyInfoFolder>
  39:           </svnupdver>
  40:         </prebuild>
  41:         
  42:         <tasks>
  43:             <!-- msbuild task help: http://confluence.public.thoughtworks.org/display/CCNET/MsBuild+Task -->
  44:             <msbuild>
  45:                 <executable>C:\WIN2003\Microsoft.NET\Framework\v3.5\MSBuild.exe</executable>
  46:                 <workingDirectory>c:\dev\solutions\SilverlightTagCloud</workingDirectory>
  47:                 <projectFile>SilverlightTagCloud.sln</projectFile>
  48:                 <buildArgs>/noconsolelogger /p:Configuration=Debug /v:Minimal</buildArgs>
  49:                 <targets>Build</targets>
  50:                 <timeout>900</timeout>
  51:                 <logger>c:\Program Files\CruiseControl.NET\server\Rodemeyer.MsBuildToCCNet.dll</logger>
  52:             </msbuild>
  53:             <msbuild>
  54:                 <executable>C:\WIN2003\Microsoft.NET\Framework\v3.5\MSBuild.exe</executable>
  55:                 <workingDirectory>c:\dev\solutions\SilverlightTagCloud</workingDirectory>
  56:                 <projectFile>SilverlightTagCloud.sln</projectFile>
  57:                 <buildArgs>/noconsolelogger /p:Configuration=Release /v:Minimal</buildArgs>
  58:                 <targets>Build</targets>
  59:                 <timeout>900</timeout>
  60:                 <logger>c:\Program Files\CruiseControl.NET\server\Rodemeyer.MsBuildToCCNet.dll</logger>
  61:             </msbuild>
  62:             <exec>
  63:                 <executable>C:\dev\deploy\ziprelease.bat</executable>
  64:                 <environment>
  65:                     <variable>
  66:                         <name>BUILDNAME</name>
  67:                         <value>SilverlightTagCloud</value>
  68:                     </variable>
  69:                 </environment>
  70:             </exec>
  71:         </tasks>
  72:         
  73:     </project>
  74:     <!-- end SilverlightTagCloud -->
  75:  
  76: </cruisecontrol>

The Task Block is the main workhorse of the CC.NET CI system. It is where you would typically put your build tasks, run unit tests, etc. For the full list of tasks, see the Task Block documentation.

As you can see above, the SilverlightTagCloud project doesn't have a lot of tasks. It builds Debug, it builds Release, and it zips the release via a batch file within an Exec Task. Other steps I like to add to the tasks section are NCover via an Exec Task (you can alternatively use the NCover Reporting Task) and NUnit Testing via the NUnit Task, but because Silverlight still has limited unit testing tools and, well, I don't see the point of adding code coverage to something that has limited unit testing to begin with, those sections are not here. In the interest of being thorough, however, I'll include examples from another project so you can at least see them defined.

First, the MSBuild Tasks' attributes:

  • executable: The location of the msbuild.exe executable.
  • workingDirectory: The project's root folder.
  • projectFile: The solution or project file to build.
  • buildArgs: Any arguments to pass to msbuild.
  • targets: A list of targets to run (i.e., Build; Test).
  • timeout: Number of seconds to wait before assuming we're hung and to time the build out.
  • logger: If you're using a custom logger, specify that here.

Now, the Exec Task. An Exec Task is a general purpose task wherein you can run just about anything you want. Here, I use it to run a batch file that zips up the project's source, build output, etc. (using the 7zip command line application). One of the nice features of CC.NET is that it will store things like the build label as an environment variable that you can then use in a batch file. I use the label as part of the zip file's name, so something like "SilverlightTagCloud v1.0.7.8.zip". This file, which constitutes a release candidate, is then stored away in a shared build archive folder.

The attributes of the Exec Task that I use are:

  • executable: The program to run.
  • environment: Used to specify environment variables to set for the running executable. Here I set BUILDNAME to "SilverlightTagCloud", which is used to construct the name of the .zip file.

Now, to throw in how I use NCover and NUnit.

For NCover, you can use the NCover Reporting Task or, as I do, an exec task (see the Using CruiseControl.NET with NCover tutorial for more info; remember to add a merge task):

   1: <exec>
   2:     <executable>"C:\Program Files\NCover\NCover.Console.exe"</executable>
   3:     <buildArgs>"C:\Program Files\NUnit 2.4.7\bin\nunit-console.exe"
   4:         //w "C:\project\solutions\projectname\bin\debug"
   5:         //x "C:\project\solutions\projectname\bin\ccnet\artifacts\ncover\project-coverage.xml"
   6:         //l "C:\project\solutions\projectname\bin\ccnet\artifacts\ncover\project-coverage.log"
   7:         "C:\project\solutions\projectname\bin\client\debug\projectname.dll"</buildArgs>
   8: </exec>

For NUnit, I use the NUnit Task:

   1: <nunit path="C:\Program Files\NUnit 2.4.7\bin\nunit-console.exe">
   2:     <assemblies>
   3:         <assembly>C:\project\solutions\projectname\bin\client\debug\projectname.dll</assembly>
   4:     </assemblies>
   5: </nunit>

8. Add Publishers

I use two publishers: one to log modification information (as in when a code file has changed) and another, the XML Log Publisher, which the CC.NET documentation states, "The Xml Log Publisher is used to create the log files used by the CruiseControl.NET Web Dashboard, so if you don't define an <xmllogger /> section the Dashboard will not function correctly". Not much more to say about it beyond that. See the conclusion for usage of the <publishers> task.

Conclusion

Here, then, is the complete ccnet.config file for the SilverlightTagCloud project:

   1: <cruisecontrol>
   2:  
   3:     <!-- begin SilverlightTagCloud -->
   4:     <!-- online help: http://confluence.public.thoughtworks.org/display/CCNET/Project+Configuration+Block -->
   5:     <project name="SilverlightTagCloud" queue="SilverlightTagCloud" queuePriority="1">
   6:         <workingDirectory>C:\dev\solutions\SilverlightTagCloud</workingDirectory>
   7:         <artifactDirectory>C:\dev\solutions\SilverlightTagCloud\bin\ccnet\artifacts</artifactDirectory>
   8:         <webURL>http://server_name_or_ip/ccnet/server/local/project/SilverlightTagCloud/ViewLatestBuildReport.aspx</webURL>
   9:         <modificationDelaySeconds>20</modificationDelaySeconds>
  10:         
  11:         <triggers>
  12:             <!-- triggers help: http://confluence.public.thoughtworks.org/display/CCNET/Interval+Trigger -->
  13:             <intervalTrigger name="continuous" seconds="300" buildCondition="IfModificationExists" />
  14:         </triggers>
  15:  
  16:         <state type="state" directory="C:\ccnetstate" />
  17:         
  18:         <sourcecontrol type="svnget">
  19:             <executable>c:\program files\VisualSVN\bin\svn.exe</executable>
  20:             <workingDirectory>c:\dev\solutions\SilverlightTagCloud</workingDirectory>
  21:         </sourcecontrol>
  22:         
  23:         <labeller type="iterationlabeller">
  24:             <prefix>1.0</prefix>
  25:             <duration>1</duration>
  26:             <releaseStartDate>2009/8/15</releaseStartDate>
  27:             <separator>.</separator>
  28:         </labeller>
  29:         
  30:         <prebuild>
  31:            <!-- assembly version update prior to build -->
  32:            <svnupdver>
  33:               <executable>c:\program files\VisualSVN\bin\svn.exe</executable>
  34:               <assemblyInfoFolder>C:\dev\solutions\SilverlightTagCloud\projects\TagCloudControl\Properties</assemblyInfoFolder>
  35:           </svnupdver>
  36:           <svnupdver>
  37:              <executable>c:\program files\VisualSVN\bin\svn.exe</executable>
  38:              <assemblyInfoFolder>C:\dev\solutions\SilverlightTagCloud\projects\TagCloudService\Properties</assemblyInfoFolder>
  39:           </svnupdver>
  40:         </prebuild>
  41:         
  42:         <tasks>
  43:             <!-- msbuild task help: http://confluence.public.thoughtworks.org/display/CCNET/MsBuild+Task -->
  44:             <msbuild>
  45:                 <executable>C:\WIN2003\Microsoft.NET\Framework\v3.5\MSBuild.exe</executable>
  46:                 <workingDirectory>c:\dev\solutions\SilverlightTagCloud</workingDirectory>
  47:                 <projectFile>SilverlightTagCloud.sln</projectFile>
  48:                 <buildArgs>/noconsolelogger /p:Configuration=Debug /v:Minimal</buildArgs>
  49:                 <targets>Build</targets>
  50:                 <timeout>900</timeout>
  51:                 <logger>c:\Program Files\CruiseControl.NET\server\Rodemeyer.MsBuildToCCNet.dll</logger>
  52:             </msbuild>
  53:             <msbuild>
  54:                 <executable>C:\WIN2003\Microsoft.NET\Framework\v3.5\MSBuild.exe</executable>
  55:                 <workingDirectory>c:\dev\solutions\SilverlightTagCloud</workingDirectory>
  56:                 <projectFile>SilverlightTagCloud.sln</projectFile>
  57:                 <buildArgs>/noconsolelogger /p:Configuration=Release /v:Minimal</buildArgs>
  58:                 <targets>Build</targets>
  59:                 <timeout>900</timeout>
  60:                 <logger>c:\Program Files\CruiseControl.NET\server\Rodemeyer.MsBuildToCCNet.dll</logger>
  61:             </msbuild>
  62:             <exec>
  63:                 <executable>C:\dev\deploy\ziprelease.bat</executable>
  64:                 <environment>
  65:                     <variable>
  66:                         <name>BUILDNAME</name>
  67:                         <value>SilverlightTagCloud</value>
  68:                     </variable>
  69:                 </environment>
  70:             </exec>
  71:         </tasks>
  72:         
  73:         <publishers>
  74:             <!-- modificationHistory help: http://confluence.public.thoughtworks.org/display/CCNET/ModificationHistory+Publisher -->
  75:             <modificationHistory onlyLogWhenChangesFound="true" />
  76:             <xmllogger logDir="buildlogs\" />
  77:             <statistics />
  78:         </publishers>
  79:         
  80:     </project>
  81:     <!-- end SilverlightTagCloud -->
  82:  
  83: </cruisecontrol>

Resources

 


Setting up a Continuous Integration System, Part 7: Installing CruiseControl.NET and Custom Plug-ins

September 10, 2009 15:08

This is the next part in an ongoing series about setting up a continuous integration system. The series includes:

  1. Part 1: Introduction
  2. Part 2: Project Folders
  3. Part 3: CI Workflow
  4. Part 4: CI Server Baseline Software
  5. Part 5: CruiseControl.NET Custom Plug-in: Update Version
  6. Part 6: CruiseControl.NET Custom Plug-in: Source Retrieval
  7. Part 7: Installing CruiseControl.NET and Custom Plug-ins (this post)
  8. Part 8: Configuring CruiseControl.NET
  9. Part 9: Conclusion

The first four posts of this series were mostly talking in abstractions, defining my project folder structure, continuous integration workflow, and naming the software I consider crucial to any CI environment. In the last two posts I made a side-trip, defining how to write CruiseControl.NET plug-ins. That was nice because it allowed us to jump back into some code. Now, though, let's get back to the CI server itself and define the steps I take to baseline the machine and get CC.NET ready to go.

Choose your OS

I use Windows Server 2003 as my build server's OS, but that's only because the machine does double-duty as a DHCP server, internet gateway, DNS server, and domain controller. But there's nothing to keep you from using Windows XP or Windows 7 as your build server.

Install CruiseControl.NET

The first step is, of course, to download and install CruiseControl.NET. I use the default install options.

Install CruiseControl.NET Plug-ins and Add-ons

See Posts 5 and 6 of this series for more info on CC.NET plug-ins.

1. Rodemeyer.MsBuildToCCnet.dll

If you're going to use MSBuild to compile projects, you might want this CruiseControl.NET logger add-on. The reasons are listed on the add-on's cc.net web page; I'll leave it to you to determine if this is something you need, but I've found it to be a big help in formatting MSBuild's output. See the cc.net web page for instructions on how to install.

2. ccnet.svnupdver.plugin.dll

There are a varying philosophies on how to go about updating assembly versions as part of the CI process. My preferred method is to check-out the AssemblyVersion.cs file, update the assembly version using the CC.NET build version, then check the file back in. That's what this custom plug-in does.

The ccnet.svnupdver.plugin.dll can be downloaded from posts 5 or 6 of this series (the .zip contains both the get and update version plug-ins for SVN and TFS). To install, copy the DLL into the CC.NET 'server' folder.

3. ccnet.svnget.plugin.dll

Sure, you can use one of the provided Source Control Blocks, or you can write your own. Better yet, you can use this one. The SVNGet plug-in is a custom CC.NET plug-in written by yours truly. It provides source control access to an SVN repository of your choice.

The reason I wrote this plug-in is this: As a Subversion user I was, at first, perfectly happy using the default Subversion Source Control Block. Until I ran into the update version, circular build loop which I've probably discussed to death at this point. See previous two posts for more information.

The ccnet.svnget.plugin.dll can be downloaded from posts 5 or 6 of this series (the .zip contains both the get and update version plug-ins for SVN and TFS). Simply copy into your 'server' folder and add the task block to your ccnet.config file (more on that next post).

Additional Software

You can pick and choose from what I listed as CI server baseline software, but, minimally, after what's listed above I always install the following:

1.) Visual Studio

2.) NUnit

3.) TestDriven.NET

4.) 7-zip

Conclusion and Next Steps

Installing the above software should get you a baseline CI machine, ready to go except for the configuration step. For CC.NET, that involves working with the ccnet.config file. Next post, I'll break down the various blocks and show how to add a basic project. Until then.


Setting up a Continuous Integration System, Part 6: CruiseControl.NET Custom Plug-in: Source Retrieval

September 10, 2009 08:05

This is the next part in an ongoing series about setting up a continuous integration system. The series includes:

  1. Part 1: Introduction
  2. Part 2: Project Folders
  3. Part 3: CI Workflow
  4. Part 4: CI Server Baseline Software
  5. Part 5: CruiseControl.NET Custom Plug-in: Update Version
  6. Part 6: CruiseControl.NET Custom Plug-in: Source Retrieval (this post)
  7. Part 7: Installing CruiseControl.NET and Custom Plug-ins
  8. Part 8: Configuring CruiseControl.NET
  9. Part 9: Conclusion

CruiseControl.NET has an extensible plug-in architecture. While documentation on how to take advantage of it is sparse, there are some sources I've found helpful: Custom Builder Plug-in, which is a tutorial on how to write a plug-in derived from ITask, and the TFS Plug-in for CruiseControl.NET project on CodePlex, which is a great code resource for writing a plug-in derived from ISourceControl.

In this post I'll discuss two custom plug-ins, both derived from ISourceControl, whose shared purpose is to retrieve source code prior to a build. The only difference is that one interfaces with Subversion while the other, Team Foundation Server. While there are obviously coding differences because of this, the general workflow of each plug-in is the same.

CruiseControl.NET Assembly References

There are minimally two dll's you will need to add as references to any CC.NET plug-in project: ThoughtWorks.CruiseControl.Core.dll and NetReflector.dll. The Core dll contains the interfaces and other goodness; NetReflector contains the attributes you'll need to mark your classes, methods, etc. so CC.NET knows what to do with it. Once you've installed CruiseControl.NET, you'll find both of these assembles in the C:\Program Files\CruiseControl.NET\server folder.

Writing a CC.NET Custom ISourceControl Plug-in

Writing an ISourceControl plug-in is very similar to creating an ITask plug-in. Rather than duplicate work by writing up on my own tutorial, I'll instead refer you to the Custom Builder Plug-in, which takes you step-by-step on how to write an ITask plug-in. Also, refer to the TFS Plug-in for CruiseControl.NET project as that contains a lot of good information.

A Word About CI Source Control Tasks

Retrieving source is the first and one of the most fundamental steps in any CI workflow process:

image

If source has not changed, CC.NET pauses and checks again later. If source has changed, it will do a 'get' and proceed with a new build. The 'get' part is where our ISourceControl-derived plug-in comes in.

Before I jump into the plug-ins, a quick word about how I solved the "update version never-ending loop" problem: as described in Part 5 of this series, my Update Version task updates each assembly's AssemblyFileVersion attribute in each of the AssemblyInfo.cs files before checking that file into source control. The next time CC.NET runs, it sees that change, picks it up, and does another build. This creates a looping situation since, once again, the version is updated and checked in so that the next time CC.NET checks for new source… I'm sure you can see where this is going. The way I solve this is to have the Update Version task add a special marker into the source file's comment on check-in. When the Get Source plug-in sees that marker, it ignores the file. If that's the only change, it goes back to sleep. No looping. Problem solved.

Get Source Plug-in for Subversion

I'll start with a class derived from ISourceControl:

   1: namespace ccnet.svnget.plugin
   2: {
   3:     [ReflectorType ("svnget")]
   4:     public class SVNGet : ISourceControl
   5:     {

Deriving from ISourceControl requires us to implement a handful of methods. We'll get to those in a minute. For now, note the ReflectorType attribute, which names the task for use by CC.NET.

Like the Update Version plug-in from the previous post, I define the special marker which this plug-in will be looking for in the source control comment:

   1: private const string _marker = "***NO_CI***";    // if present in comment, ignore file

A few properties:

   1: /// <summary>
   2: /// Subversion username, can be set in TortoiseSVN settings if installed.
   3: /// </summary>
   4: [ReflectorProperty ("username", Required = false)]
   5: public string Username { get; set; }
   6:  
   7: /// <summary>
   8: /// Subversion password, can be set in TortoiseSVN settings if installed.
   9: /// </summary>
  10: [ReflectorProperty ("password", Required = false)]
  11: public string Password { get; set; }
  12:  
  13: /// <summary>
  14: /// The local working folder where Subversion commands will be run.
  15: /// </summary>
  16: [ReflectorProperty ("workingDirectory")]
  17: public string WorkingDirectory;
  18:  
  19: /// <summary>
  20: /// The SVN executable, including path if necessary.
  21: /// </summary>
  22: [ReflectorProperty ("executable")]
  23: public string Executable;

Similar to how the ReflectorType attribute defines the CC.NET task, ReflectorProperty defines properties for the task. Note that if you are using Visual SVN/TortoiseSVN, you can set your credentials through those applications and do not have to add them as properties in your ccnet.config file.

Now, there are a number of methods you must implement when deriving from ISourceControl. Here's a few I didn't do much with:

   1: public void LabelSourceControl (IIntegrationResult result)
   2: {
   3:     return;
   4: }
   5:  
   6: public void Initialize (IProject project)
   7: {
   8:     return;
   9: }
  10:  
  11: public void Purge (IProject project)
  12: {
  13:     return;
  14: }

Let's move on to something more interesting…

GetModifications is an ISourceControl method we implement to retrieve a list of modified files:

   1: /// <summary>
   2: /// Retrieves a list of modified files from Subversion.
   3: /// </summary>
   4: /// <param name="from"></param>
   5: /// <param name="to"></param>
   6: /// <returns>A list of files modified in source control.</returns>
   7: public Modification[] GetModifications (IIntegrationResult from, IIntegrationResult to)
   8: {
   9:     var modifications = new List<Modification> ();
  10:  
  11:     if (!GetModifiedFiles (ref modifications))
  12:     {
  13:         Log.Info ("Failed checking for source modifications.");
  14:     }
  15:  
  16:     return (modifications.ToArray ());
  17: }

All of the works happens in GetModifiedFiles, which is jam-packed with action. It's a big method, too. I'll go over it in pieces.

The first thing I do is run an SVN status command using our RunProcess helper (see source code download below for more info on how RunProcess works), using the –u flag to retrieve file status for those files that have changed in the repository:

   1: argBuilder = new ProcessArgumentBuilder ();
   2: argBuilder.AppendArgument ("status -u");        // -u will give us an '*' for those files that have changed on the server
   3: argBuilder.AppendArgument (WorkingDirectory);
   4: AppendCommonSwitches (argBuilder);
   5:  
   6: var result = RunProcess (argBuilder);

What this does is give us a list of potentially changed files which we'll then iterate through (this is the first part of the iteration loop):

   1: var potentiallyModifiedItems = result.StandardOutput.Split (newline);
   2: foreach (var svnItem in potentiallyModifiedItems)
   3: {
   4:     // the standard output contains double end of line's, so we'll wind up with some blank entries here
   5:     if (string.IsNullOrEmpty (svnItem)) continue;
   6:  
   7:     if (svnItem[0] == '?') continue;    // item not under source control
   8:  
   9:     // 'M' indicates modified item
  10:     if (svnItem[8] != '*') continue;    // no '*' = file has not changed on server
  11:  
  12:     var file = svnItem.Substring (21);    // full path + file (if not a directory) starts at the 21st 0-indexed position
  13:  
  14:     // need to examine log for our marker, which if found will tell us to ignore this changed file
  15:     argBuilder = new ProcessArgumentBuilder ();
  16:     argBuilder.AppendArgument ("log -r HEAD");    // get the latest (i.e., HEAD) log info
  17:     argBuilder.AppendArgument (file);
  18:     AppendCommonSwitches (argBuilder);
  19:  
  20:     result = RunProcess (argBuilder);

We filter out some of the results, then, once we have a candidate file, we examine the source comment for our special marker. If the marker is found, we ignore the file, otherwise we process it:

   1: if (!result.StandardOutput.Contains (_marker))
   2: {
   3:     // modified file has not been "marked", so add to modifications list
   4:  
   5:     // example log output (the whole thing is contained within 'infoLines'):
   6:     //
   7:     // ------------------------------------------------------------------------
   8:     // r309 | scottfm | 2009-08-28 20:57:18 -0500 (Fri, 28 Aug 2009) | 1 line        <-- Line 1; split into modificationAttributes
   9:     //
  10:     // ***NO_CI*** AssemblyFileVersion updated from version [1.0.1.8] to [1.0.1.9].    <-- Line 3
  11:     // ------------------------------------------------------------------------
  12:  
  13:     var infoLines = result.StandardOutput.Split (newline);
  14:     var infoLinesList = new List<string>();
  15:  
  16:     // a lot of massaging of stdout output... see download for full source
  17:  
  18:     // version info
  19:     var versionString = modificationAttributes[0].Substring (1);
  20:     var version = Convert.ToInt32 (versionString);
  21:  
  22:     // modified date/time
  23:     var findFirstSpace = modificationAttributes[2].IndexOf (' ');
  24:     var findSecondSpace = modificationAttributes[2].IndexOf (' ', findFirstSpace + 1);
  25:     var datetime = modificationAttributes[2].Substring (0, findSecondSpace);
  26:     var modifiedDateTime = DateTime.Parse (datetime);
  27:  
  28:     var modification = new Modification
  29:                            {
  30:                                UserName = modificationAttributes[1],
  31:                                Comment = infoLines[3],
  32:                                ChangeNumber = version,
  33:                                ModifiedTime = modifiedDateTime,
  34:                                Version = versionString,
  35:                                Type = string.Empty,
  36:                                FileName = Path.GetFileName (file),
  37:                                FolderName = Path.GetDirectoryName (file)
  38:                            };
  39:  
  40:     modifiedFilesList.Add (modification);

There's a lot of massaging of the stdout output; I left that out of the above snippet so we can get to the good stuff, which is to build up a Modification object and add it to our modifiedFilesList collection. Since the collection was passed in using the 'ref' keyword, the list we build here is passed back by reference to GetModifications whereupon it is returned to CC.NET.

The last part of this plug-in is to actually get the source from SVN so CC.NET can build it. We do that with the ISourceControl method, GetSource (It's of some confusion when coming from the Visual SourceSafe or TFS world that the equivalent of a 'get' is actually termed an 'update' in SVN). In GetSource, we'll perform an SVN update in order to retrieve files that been modified and checked-in to source control:

   1: /// <summary>
   2: /// Retrieves changed files from source control.
   3: /// </summary>
   4: /// <param name="result"></param>
   5: public void GetSource (IIntegrationResult result)
   6: {
   7:     // we'll run an svn update to retrieve modified files
   8:  
   9:     var argBuilder = new ProcessArgumentBuilder ();
  10:     argBuilder.AppendArgument ("update");
  11:     argBuilder.AppendArgument (WorkingDirectory);
  12:     AppendCommonSwitches (argBuilder);
  13:  
  14:     var runProcessResult = RunProcess (argBuilder);
  15:  
  16:     Debug.WriteLine (string.Format ("SVN update output [{0}]", runProcessResult.StandardOutput));
  17:     Log.Debug (string.Format ("SVN update output [{0}]", runProcessResult.StandardOutput));
  18:  
  19:     return;
  20: }

RunProcess runs an SVN update on our working directory. This has the effect of retrieving any source that has been modified since the last time we did an update. With that, we're ready for CC.NET to perform our build.

For reference, here's the task as defined in the ccnet.config file:

   1: <sourcecontrol type="svnget">
   2:     <executable>c:\program files\VisualSVN\bin\svn.exe</executable>
   3:     <workingDirectory>c:\dev\solutions\SilverlightTagCloud</workingDirectory>
   4: </sourcecontrol>

I'll be going over CC.NET's ccnet.config file in Part 8 of this series.

That's it for the Get Source SVN plug-in.

Get Source Plug-in for TFS

I won't go into a lot of detail on the Get Source Plug-in for TFS as it is very similar to the Update Version Plug-in for TFS discussed in my last post. Besides for that, I didn't write this plug-in. Credit Martin Woodward with that Herculean feat. I did, however, make one small change to his code to handle my special marker scenario.

That code is:

   1: // 2009-05-01 - sfm - ignore code change if comment contains "***NO_CI***"; this will prevent recursive builds on version updating
   2: Log.Debug (string.Format ("Checking for NO_CI flag for project [{0}]...", from.ProjectName));
   3: if (comment.Contains ("***NO_CI***"))
   4: {
   5:     Log.Info ("Ignoring file");
   6:     continue;
   7: }

Which quite simply looks for the marker. If it finds it, the file is ignored inasmuch as the change set is concerned.

For reference, here is how one might define this task in CC.NET's ccnet.config file:

   1: <sourcecontrol type="vsts" autoGetSource="true" applyLabel="false">
   2:     <server>http://tfs-server/</server>
   3:     <project>$/MyProject</project>
   4:     <workingDirectory>c:\MyProject</workingDirectory>
   5:     <workspace>CCNET</workspace>
   6: </sourcecontrol>

Conclusion

This plug-in stuff is exhausting! But we're done now. Hopefully between this post and the last one you have enough information to write just about any CruiseControl.NET plug-in you might ever need.

Note that the download contains my modified version of Martin Woodward's TFS Plug-in for CruiseControl.NET. I can't guarantee I have the latest and greatest source for that, so visit the CodePlex site if you want to start with his newest code.

Also, the CCNETPlugins download contains both the Get Source and Update Version plug-ins.

Download: CCNETPlugins.zip

Resources

NetReflector: One Minute Introduction

Custom Builder Plug-in

TFS Plug-in for CruiseControl.NET

VSTSPlugins