Friday, 19 September 2008

How to Label CruiseControl.Net Builds with Vault Folder Version Number

I've just accomplished a long-standing ambition. You'll laugh when I tell you what it is, and label me a geek; but I don't care: I'm a satisfied man. I've just figured out how to get the Build number of my software (you know - the fourth component that comes after Major.Minor.Revision) to tie up with the version number that our Source Control system assigns to the trunk folder of the project. Now, whenever I'm looking at a version of our software, I'll be able to tell exactly which version of the source code produced it.

We use CruiseControl.Net, running on an independent Build Server to rebuild the software every time we check something into the Source Control System, Sourcegear Vault (both excellent products by the way, highly recommended). CruiseControl is very configurable, and one of the things you can change is how it labels the builds it generates. For a while it has had the LastChangeLabeller which does almost what I want, but when used with Vault it generates labels based on the Transaction Id given to each check-in, not the Folder version number.

Earlier this year, Sourcegear produced their own CruiseControl plug-in, improving on the one that's included in CruiseControl. After Reflectoring through that, I at last figured out a way of implementing my own Labeller plug-in to do exactly what I want.

Follow in my Footsteps

  1. Download my ccnet.vaultlabeller.plugin.dll, and copy it to the CruiseControl.Net\server folder in Program Files
  2. Make sure you've configured CruiseControl to work with the the vaultplugin or fortressplugin: my labeller needs the extra information that they supply to do its stuff.
  3. Add this extra section to your project configuration in cruisecontrol (or use it to replace any current labeller block that you have). If you want a prefix included in the label, add it to the prefix element.

    <labeller type="vaultFolderVersionLabeller">
      <prefix></prefix>
    </labeller>

  4. Make sure your MSBuild or NAnt scripts are configured to get their Build number from the CCNetLabel or CCNetNumericLabel properties. For example, each of the projects in my solution is linked to a common file SharedAssemblyVersion.cs containing just the [AssemblyVersion] and [AssemblyFileVersion] attributes. Then my CruiseControl project is configured to run the following MsBuild target before doing the main build (note that it uses MsBuild Community tasks):

    <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
      <Import Project="MSBuild.Community.Tasks.Targets"/>
      
      <PropertyGroup>
        <BuildNumber>*</BuildNumber>
        <BuildNumber Condition="$(CCNetNumericLabel) != 0">$(CCNetNumericLabel)</BuildNumber>
      </PropertyGroup>
      
      <Target Name="UpdateSharedBuildNumber">
        <!-- Update the Build number of the assembly from that passed by CruiseControl-->
        <FileUpdate Condition=" $(BuildNumber) != '*'" ContinueOnError="true" Files="..\Shared\SharedAssemblyVersion.cs" Regex="(AssemblyVersion\(\&quot;\d+)\.(\d+)\.(\d+)\.\*" ReplacementText="$1.$2.$3.$(BuildNumber)" />
        <FileUpdate Condition=" $(BuildNumber) != '*'" ContinueOnError="true" Files="..\Shared\SharedAssemblyVersion.cs" Regex="(AssemblyFileVersion\(\&quot;\d+)\.(\d+)\.(\d+)\.." ReplacementText="$1.$2.$3.$(BuildNumber)" />
      </Target>
    </Project>

  5. Restart the CruiseControl.Net Service so that it picks up the changes, and you're done.

The Source Code

Creating a plug-in for CruiseControl.Net is easy. To a Visual Studio library project, add a reference to ThoughtWorks.CruiseControl.Core and NetReflector which you'll find in the the CruiseControl.Net\server folder. In this case I also needed to add a reference to the ccnet.fortressvault.plugin that SourceGear supplied. Then the plugin code is contained in one class:

using Exortech.NetReflector;
using ThoughtWorks.CruiseControl.Core;
using ThoughtWorks.CruiseControl.Core.Util;

namespace Paragon.CCNet.Label
{
    /// <summary>
    /// A CruiseControl Labeller that works in conjunction with the SourceGear Vault SourceControl plugin ("vaultplugin")
    /// Generates Labels based on the version number of the root folder.
    /// </summary>
    [ReflectorType("vaultFolderVersionLabeller")]
    public class VaultFolderVersionLabeller : ILabeller
    {
        public void Run(IIntegrationResult result)
        {
            result.Label = Generate(result);
        }

        public string Generate(IIntegrationResult integrationResult)
        {
           string label = integrationResult.LastIntegration.Label;

            if (integrationResult.HasModifications())
            {
                Modification[] modifications = integrationResult.Modifications;
                FortressModification modification = modifications[modifications.Length - 1] as FortressModification;

                if (modification != null)
                {
                    label = Prefix + modification.RootFolderVersion;
                }
                else
                {
                    Log.Warning("Vault modification info was not found: vaultFolderVersionLabeller should only be used with vaultplugin or fortressplugin");
                    label = Prefix;
                }
            }
            else
            {
                Log.Debug("No modifications found: reusing previous label.");
            }

            return label;
        }

        [ReflectorProperty("prefix", Required=false)]
        public string Prefix = string.Empty;
    }
}

A couple of Notes:

  • You need to decorate your plugin class with the ReflectorType attribute (see line 11): the value you give is the one that you'll use in the ccnet configuration file to specify which plugin to use
  • Any configuration properties of the class need to be decorated with the ReflectorProperty attribute (as in line 46).

3 comments:

Anonymous said...

Thanks for this post.Please could you provide a note on how to go about it if you don't have the shared linked file (step 4).

Sam said...

If you don't have a shared file holding the AssemblyVersion attribute you'll need to update the AssemblyInfo.cs file of each project individually. The easiest way to do that is probably to list the individual files in the Files attribute of the FileUpdate elements that I've shown above: you separate individual files using a semi-colon.

E.g. <FileUpdate Condition=" $(BuildNumber) != '*'" ContinueOnError="true" Files="\ProjectA\Properties\AssemblyInfo.cs;\ProjectB\Properties\AssemblyInfo.cs;ProjectC\Properties\AssemblyInfo.cs" Regex="(AssemblyVersion\(\"\d+)\.(\d+)\.(\d+)\.\*" ReplacementText="$1.$2.$3.$(BuildNumber)" />

Does that help?

Anonymous said...

Thanks for this post.Please could you provide a note on how to go about it if you don't have the shared linked file (step 4).

Post a Comment