Setting up a Continuous Integration System, Part 5: CruiseControl.NET Custom Plug-in: Update Version

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 (this post)
  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

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.

ITask and ISourceControl are interfaces contained within the ThoughtWorks.CruiseControl.Core namespace. While there are other interfaces available, I’ll focus on just those two as those are the ones I’ve found most useful.

In this post I’ll discuss two custom plug-ins, both derived from ITask, whose shared purpose is to update an assembly version prior to build. The only difference is that one interfaces with Subversion while the other, Team Foundation Server.

Next post I’ll deal with ISourceControl and two plug-ins that individually pull source from Team Foundation Server and Subversion.

For now, though, ITask

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 FilesCruiseControl.NETserver folder.

Writing a CC.NET Custom ITask Plug-in

Rather than write up my own tutorial for something that’s been done before, I’ll instead refer you to the Custom Builder Plug-in (which, IMO, should be renamed to “Custom ITask-derived Plug-in”) tutorial. That leaves me free to jump right into my own ITask-derived plug-in, Update Version.

A Word About CI Version Updating

You’ll recall that in my CI Workflow I perform an assembly version update prior to performing a build:


That basic function of the Update Version task is to stamp the CC.NET build version onto the project’s assemblies via the AssemblyFileVersion attribute in the AssemblyInfo.cs file. What the flowchart doesn’t show is that the Update Version step also does a check-in of the modified AssemblyInfo.cs file back to source control. This creates an interesting/annoying problem in that the next time CC.NET checks for modified files, it picks up the just changed AssemblyInfo.cs file and goes about its business of performing a build. Of course, the version is updated again, AssemblyInfo.cs is checked-in, and here we go again and again and again. I stop this loop by placing a special marker in the check-in comment:

   1: private const string _marker = "***NO_CI***";
View Plain

This special marker is looked for by the ‘get’ task (the “Source Checked In?” step above). If it is found, the file is ignored and not added to the modified file list. Simple as that.

With that small annoyance handled, let’s take a look at the SVN flavor of Update Version.

Update Version Plug-in for Subversion

First off, we define our class, labeling it with the ReflectorType attribute and deriving from ITask:

   1: namespace ccnet.svnupdver.plugin
   2: {
   3:     [ReflectorType ("svnupdver")]
   4:     public class SVNUpdVer : ITask
   5:     {
View Plain

We also have some properties, marked with ReflectorProperty, which we’ll see below makes them settable from CC.NET’s ccnet.config file:

   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; }
   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; }
  13: /// <summary>
  14: /// Location of the assembly's AssemblyInfo.cs file.
  15: /// </summary>
  16: [ReflectorProperty ("assemblyInfoFolder")]
  17: public string AssemblyInfoFolder;
  19: /// <summary>
  20: /// The SVN executable, including path if necessary.
  21: /// </summary>
  22: [ReflectorProperty ("executable")]
  23: public string Executable;
View Plain

Since this is an ITask-derived plug-in, we’ll implement the Run method:

   1: /// <summary>
   2: /// Primary ITask-derived function. Updates assembly version and checks modified file into SVN source control.
   3: /// </summary>
   4: /// <param name="integrationResult">Integration result passed in from CruiseControl.NET. Contains version information.</param>
   5: public void Run (IIntegrationResult integrationResult)
   6: {
View Plain

The Run operation performs the primary work of updating the assembly file version attribute and checking the file into source control. Here is the version update part of the function:

   1: // modify AssemblyFileVersion and write to file
   2: using (var sw = new StreamWriter (tempAssemblyInfoFile, false))    // overwrite existing file
   3: {
   4:     using (var sr = File.OpenText (localAssemblyInfoFile))
   5:     {
   6:         string line;
   8:         while ((line = sr.ReadLine ()) != null)
   9:         {
  10:             if (line.Contains (_attributeAssemblyFileVersion)) // found assembly file version?
  11:             {
  12:                 // parse out current ('old') version for information purposes
  13:                 var nPos1 = line.IndexOf ("("");
  14:                 var nPos2 = line.IndexOf ("")");
  16:                 oldVersion = line.Substring (nPos1 + 2, nPos2 - nPos1 - 2);
  18:                 Log.Debug ("Writing out new file version [" + integrationResult.Label + "]...");
  20:                 // write new version passed in via
  21:                 sw.WriteLine (string.Format ("[assembly: AssemblyFileVersion ("{0}")]", integrationResult.Label));
  22:             }
  23:             else
  24:             {
  25:                 // just write line to file
  26:                 sw.WriteLine (line);
  27:             }
  28:         }
  29:     }
  31:     sw.Close ();
  32: }
View Plain

It’s really just basic file and string manipulation.

The check-in code is a bit more interesting:

   1: // create check-in comment
   2: var comment = string.Format (""{0} AssemblyFileVersion updated from version [{1}] to [{2}]."", _marker, oldVersion, integrationResult.Label);
   4: // check AssemblyInfo.cs back in
   5: Log.Info ("Checking in new AssemblyInfo file with comment [" + comment + "]...");
   7: // check-in modified assemblyinfo file
   8: if (CheckIn (comment))
   9: {
  10:     Log.Info ("Version updated successfully.");
  11:     return;
  12: }
  14: Log.Info ("Version update failed.");
View Plain

You’ll see that on Line 2 we use our special marker as part of the change comment.

Taking a closer look at the CheckIn operation:

   1: /// <summary>
   2: /// Check-in assembly info file containing new assembly version. Gets the latest Subversion revision by checking the last log entry.
   3: /// </summary>
   4: /// <param name="comment">The comment to use for check-in.</param>
   5: /// <returns>True on success, false otherwise.</returns>
   6: private bool CheckIn (string comment)
   7: {
   8:     try
   9:     {
  10:         // Set up the command-line arguments required
  11:         var argBuilder = new ProcessArgumentBuilder ();
  12:         argBuilder.AppendArgument ("commit");
  13:         argBuilder.AppendArgument ("-m");
  14:         argBuilder.AppendArgument (comment);
  16:         if (!string.IsNullOrEmpty (Username) && !string.IsNullOrEmpty (Password))
  17:         {
  18:             AppendCommonSwitches (argBuilder);
  19:         }
  21:         argBuilder.AddArgument (AssemblyInfoFolder + _assemblyInfoFilename);
  23:         var runProcessResult = RunProcess (argBuilder);
  25:         Debug.WriteLine (string.Format ("SVN commit output [{0}]", runProcessResult.StandardOutput));
  26:         Log.Debug (string.Format ("SVN commit output [{0}]", runProcessResult.StandardOutput));
  27:     }
  28:     catch (Exception xcpt)
  29:     {
  30:         Log.Error (string.Format ("CheckIn process failed on exception [{0}]", xcpt));
  31:         return (false);
  32:     }
  34:     return (true);
  35: }
View Plain

This is where we build up a ProcessArgumentBuilder object which we then pass to RunProcess:

   1: /// <summary>
   2: /// Runs the Subversion process using the specified arguments.
   3: /// </summary>
   4: /// <param name="arguments">The Subversion client arguments.</param>
   5: /// <returns>The results of running the process, including captured output.</returns>
   6: private ProcessResult RunProcess (ProcessArgumentBuilder arguments)
   7: {
   8:     if (string.IsNullOrEmpty (Executable)) return (null);
  10:     Log.Debug (string.Format ("Running [{0}] with arguments [{1}].", Executable, arguments));
  11:     var info = new ProcessInfo (Executable, arguments.ToString (), null);
  13:     var executor = new ProcessExecutor ();
  14:     return (executor.Execute (info));
  15: }
View Plain

Executable is defined as a required ReflectorProperty property, so it should have been set in the config file (CC.NET will error out if it has not been set). Here we basically use ProcessExecutor paired with the supplied ProcessArgumentBuilder arguments to perform an SVN commit operation.

With that, we are done with the SVN Update Version plug-in.

Update Version Plug-in for TFS

The workflow for this flavor of the Update Version plug-in is very similar to the plug-in for SVN. The only real difference is the code to integrate specifically with Team Foundation Server. For that, I borrowed heavily from the TFS Plug-in for CruiseControl.NET project.

First, you need to pull in the TFS namespaces and add the corresponding assembly references:

   1: using Microsoft.TeamFoundation.Client;
   2: using Microsoft.TeamFoundation.VersionControl.Client;
View Plain

Then, we have some new ReflectorProperty properties:

   1: private string _workspaceName;
   2: /// <summary>
   3: /// Name of the workspace to create.  This will revert to the _defaultWorkspaceName if not passed.
   4: /// </summary>
   5: [ReflectorProperty ("workspace", Required = false)]
   6: public string Workspace
   7: {
   8:     get
   9:     {
  10:         if (_workspaceName == null)
  11:         {
  12:             _workspaceName = _defaultWorkspaceName;
  13:         }
  15:         return (_workspaceName);
  16:     }
  17:     set
  18:     {
  19:         _workspaceName = value;
  20:     }
  21: }
  23: /// <summary>
  24: /// The name or URL of the team foundation server.
  25: /// </summary>
  26: [ReflectorProperty ("server")]
  27: public string Server;
  29: /// <summary>
  30: /// The path to the project in source control, for example $VSTSPlugins
  31: /// </summary>
  32: [ReflectorProperty ("project")]
  33: public string ProjectPath;
  35: [ReflectorProperty ("assemblyInfoFolder")]
  36: public string AssemblyInfoFolder;
View Plain

TFS has this concept of “workspaces”, where you map a workspace to a location on your local drive. The Workspace property addresses that need. Other properties cover the TFS server, project, and our familiar assemblyInfoFolder, which is where the assembly’s AssemblyInfo.cs file lives.

Here’s the code that does the check-in:

   1: // create checkin comment
   2: var sb = new StringBuilder ();
   3: sb.AppendFormat("***NO_CI*** AssemblyFileVersion updated from version [{0}] to [{1}].", strOldVersion, result.Label);
   4: var comment = sb.ToString ();
   6: // check AssemblyInfo.cs back in
   7: Log.Info ("Checking in new AssemblyInfo file with comment [" + comment + "]...");
   9: var pendingChanges = workspace.GetPendingChanges ();
  10: workspace.CheckIn (pendingChanges, comment);
View Plain

The biggest difference here is that the CheckIn method is part of the Microsoft.TeamFoundation.VersionControl.Client namespace, so there’s nothing for us to do but call it.

Adding a Custom Task to CruiseControl.NET’s ccnet.config

Here is an example of adding the SVNUpdVer task to CruiseControl.NET’s ccnet.config file using my Silverlight Tag Cloud project as an example:

   1: <svnupdver>
   2:   <executable>c:program filesVisualSVNbinsvn.exe</executable>
   3:   <assemblyInfoFolder>C:devsolutionsSilverlightTagCloudprojectsTagCloudControlProperties</assemblyInfoFolder>
   4: </svnupdver>
View Plain

This goes in the <tasks> section. Both <executable> and <assemblyInfoFolder> match up with ReflectorProperties defined in the plug-in code. What you don’t see are username and password, both of which are not required parameters as defined by the plug-in and which I instead set as part of my TortoiseSVN settings. I like this approach better because I do not have to then put these values into the config unencrypted, nor do I have to worry about managing encryption/decryption code if I wanted to encrypt those entries.