Version Assemblies with TFS 2010 Continuous Integration

When I first heard that TFS 2010 had moved to Workflow Foundation for Team Build, I was extremely skeptical. I've loved MSBuild and didn't quite understand the reasons for this change. In fact, given that I've been exclusively using Cruise Control for Continuous Integration (CI) for the last 5+ years of my career, I was skeptical of TFS for CI in general. However, after going through the learning process for TFS 2010 recently, I'm starting to become a believer. I'm also starting to see some of the benefits with Workflow Foundation for the overall processing because it gives you constructs not available in MSBuild such as parallel tasks, better control flow constructs, and a slightly better customization story.

The first customization I had to make to the build process was to version the assemblies of my solution. This is not new. In fact, I'd recommend reading Mike Fourie's well known post on Versioning Code in TFS before you get started. This post describes several foundational aspects of versioning assemblies regardless of your version of TFS. The main points are: 1) don't use source control operations for your version file, 2) use a schema like ...0, and 3) do not keep AssemblyVersion and AssemblyFileVersion in sync.

To do this in TFS 2010, the best post I've found has been Jim Lamb's post of building a custom TFS 2010 workflow activity. Overall, this post is excellent but the primary issue I have with it is that the assembly version numbers produced are based in a date and look like this: "2010.5.15.1". This is definitely not what I want. I want to be able to communicate to the developers and stakeholders that we are producing the "1.1 release" or "1.2 release" – which would have an assembly version number of "1.1.317.0" for example.

In this post, I'll walk through the process of customizing the assembly version number based on this method – customizing the concepts in Lamb's post to suit my needs. I'll also be combining this with the concepts of Fourie's post – particularly with regards to the standards around how to version the assemblies.

The first thing I'll do is add a file called SolutionAssemblyVersionInfo.cs to the root of my solution that looks like this:

  
using System;  
using System.Reflection;  
[assembly: AssemblyVersion("1.1.0.0")]
[assembly: AssemblyFileVersion("1.1.0.0")]

I'll then add that file as a Visual Studio link file to each project in my solution by right-clicking the project, "Add – Existing Item…" then when I click the SolutionAssemblyVersionInfo.cs file, making sure I "Add As Link":

Now the Solution Explorer will show our file. We can see that it's a "link" file because of the black arrow in the icon within all our projects.

sol explorer with ver. file

Of course you'll need to remove the AssemblyVersion and AssemblyFileVersion attributes from the AssemblyInfo.cs files to avoid the duplicate attributes since they now leave in the SolutionAssemblyVersionInfo.cs file. This is an extremely common technique so that all the projects in our solution can be versioned as a unit.

At this point, we're ready to write our custom activity. The primary consideration is that I want the developer and/or tech lead to be able to easily be in control of the Major.Minor and then I want the CI process to add the third number with a unique incremental number. We'll leave the fourth position always "0" for now – it's held in reserve in case the day ever comes where we need to do an emergency patch to Production based on a branched version.

Similar to Lamb's post, I'm going to write two custom workflow activities. The "outer" activity (a xaml activity) will be pretty straight forward. It will check if the solution version file exists in the solution root and, if so, delegate the replacement of version to the AssemblyVersionInfo activity which is a CodeActivity highlighted in red below:

version xaml

Notice that the arguments of this activity are the "solutionVersionFile" and "tfsBuildNumber" which will be passed in. The tfsBuildNumber passed in will look something like this: "CI_MyApplication.4" and we'll need to grab the "4" (i.e., the incremental revision number) and put that in the third position. Then we'll need to honor whatever was specified for Major.Minor in the SolutionAssemblyVersionInfo.cs file. For example, if the SolutionAssemblyVersionInfo.cs file had "1.1.0.0" for the AssemblyVersion (as shown in the first code block near the beginning of this post), then we want to resulting file to have "1.1.4.0".

Before we do anything, let's put together a unit test for all this so we can know if we get it right:

  
[TestMethod]
public void Assembly_version_should_be_parsed_correctly_from_build_name()  
{
    // arrange
    const string versionFile = "SolutionAssemblyVersionInfo.cs";
    WriteTestVersionFile(versionFile);
    var activity = new VersionAssemblies();
    var arguments = new Dictionary<string, object> {
                        { "tfsBuildNumber", "CI_MyApplication.4"},
                        { "solutionVersionFile", versionFile}
    };
 
    // act
    var result = WorkflowInvoker.Invoke(activity, arguments);
 
    // assert
    Assert.AreEqual("1.2.4.0", (string)result["newAssemblyFileVersion"]);
    var lines = File.ReadAllLines(versionFile);
    Assert.IsTrue(lines.Contains("[assembly: AssemblyVersion("1.2.0.0")]"));
    Assert.IsTrue(lines.Contains("[assembly: AssemblyFileVersion("1.2.4.0")]"));
}

private void WriteTestVersionFile(string versionFile)  
{
    var fileContents = "using System.Reflection;n" +
        "[assembly: AssemblyVersion("1.2.0.0")]n" +
        "[assembly: AssemblyFileVersion("1.2.0.0")]";
    File.WriteAllText(versionFile, fileContents);
}

At this point, the code for our AssemblyVersion activity is pretty straight forward:

  
[BuildActivity(HostEnvironmentOption.Agent)]
public class AssemblyVersionInfo : CodeActivity  
{
    [RequiredArgument]
    public InArgument<string> FileName { get; set; }
 
    [RequiredArgument]
    public InArgument<string> TfsBuildNumber { get; set; }
 
    public OutArgument<string> NewAssemblyFileVersion { get; set; }
 
    protected override void Execute(CodeActivityContext context)
    {
        var solutionVersionFile = this.FileName.Get(context);

        // Ensure that the file is writeable
        var fileAttributes = File.GetAttributes(solutionVersionFile);
        File.SetAttributes(solutionVersionFile, fileAttributes & ~FileAttributes.ReadOnly);
 
        // Prepare assembly versions
        var majorMinor = GetAssemblyMajorMinorVersionBasedOnExisting(solutionVersionFile);
        var newBuildNumber = GetNewBuildNumber(this.TfsBuildNumber.Get(context));
        var newAssemblyVersion = string.Format("{0}.{1}.0.0", majorMinor.Item1, majorMinor.Item2);
        var newAssemblyFileVersion = string.Format("{0}.{1}.{2}.0", majorMinor.Item1, majorMinor.Item2, newBuildNumber);
        this.NewAssemblyFileVersion.Set(context, newAssemblyFileVersion);
 
        // Perform the actual replacement
        var contents = this.GetFileContents(newAssemblyVersion, newAssemblyFileVersion);
        File.WriteAllText(solutionVersionFile, contents);
 
        // Restore the file's original attributes
        File.SetAttributes(solutionVersionFile, fileAttributes);
    }
 
    #region Private Methods
 
    private string GetFileContents(string newAssemblyVersion, string newAssemblyFileVersion)
    {
        var cs = new StringBuilder();
        cs.AppendLine("using System.Reflection;");
        cs.AppendFormat("[assembly: AssemblyVersion("{0}")]", newAssemblyVersion);
        cs.AppendLine();
        cs.AppendFormat("[assembly: AssemblyFileVersion("{0}")]", newAssemblyFileVersion);
        return cs.ToString();
    }
 
    private Tuple<string, string> GetAssemblyMajorMinorVersionBasedOnExisting(string filePath)
    {
        var lines = File.ReadAllLines(filePath);
        var versionLine = lines.Where(x => x.Contains("AssemblyVersion")).FirstOrDefault();
 
        if (versionLine == null)
        {
            throw new InvalidOperationException("File does not contain [assembly: AssemblyVersion] attribute");
        }
 
        return ExtractMajorMinor(versionLine);
    }
 
    private static Tuple<string, string> ExtractMajorMinor(string versionLine)
    {
        var firstQuote = versionLine.IndexOf('"') + 1;
        var secondQuote = versionLine.IndexOf('"', firstQuote);
        var version = versionLine.Substring(firstQuote, secondQuote - firstQuote);
        var versionParts = version.Split('.');
        return new Tuple<string, string>(versionParts[0], versionParts[1]);
    }
 
    private string GetNewBuildNumber(string buildName)
    {
        return buildName.Substring(buildName.LastIndexOf(".") + 1);
    }
 
    #endregion
}

At this point the final step is to incorporate this activity into the overall build template. Make a copy of the DefaultTempate.xaml – we'll call it DefaultTemplateWithVersioning.xaml. Before the build and labeling happens, drag the VersionAssemblies activity in. Then set the LabelName variable to* "BuildDetail.BuildDefinition.Name + "-" + newAssemblyFileVersion* since the newAssemblyFileVersion was produced by our activity.

tfs version templ

Once you add your solution to source control, you can configure CI with the build definition window as shown here. The main difference is that we'll change the Process tab to reflect a different build number format and choose our custom build process file:

build process tab

When the build completes, we'll see the name of our project with the unique revision number:

tfs completed builds

If we look at the detailed build log for the latest build, we'll see the label being created with our custom task:

 tfs build log

We can now look at the history labels in TFS and see the project name with the labels (the Assignment activity I added to the workflow):

 tfs labels

Finally, if we look at the physical assemblies that are produced, we can right-click on any assembly in Windows Explorer and see the assembly version in its properties:

assembly props

We now have full traceability for our code. There will never be a question of what code was deployed to Production. You can always see the assembly version in the properties of the physical assembly. That can be traced back to a label in TFS where the unique revision number matches. The label in TFS gives you the complete snapshot of the code in your source control repository at the time the code was built. This type of process for full traceability has been used for many years for CI – in fact, I've done similar things with CCNet and SVN for quite some time. This is simply the TFS implementation of that pattern. The new features that TFS 2010 give you to make these types of customizations in your build process are quite easy once you get over the initial curve.