I’ve written in the past on how to do automatic assembly versioning with TFS Team Build 2008 so it’s about time I got around to showing you how to do with with TFS Team Build 2010.

By the way, for a great series of blog posts on customising team build with TFS 2010 it’s worth having a look at Ewald Hofman’s series on customising team build.

What We Want to Achieve

So, the goal here is to change the standard build process to enable us to do automatic versioning of assemblies.  To do this what we need to do is insert some steps in the process just after the source has been retrieved from TFS.  We want a flow that goes something like this:

Get Latest –> Update AssemblyInfo files with new version –> Do rest of the build

One extra wrinkle to this is that when team build gets the code from TFS the files are in read only mode, so what we’re going to do is switch the read only flag off, change the files, and then turn it back on.  This is just in case an future incremental get tries to update the file – the read only flag will stop TFS checking if the file is different.

One thing we’re not going to do is check the updated assembly info files back into source control.  This means all developer builds will by default have a version number of 1.0.0.0 and build server builds will have an incrementing build number.  It’s a handy way to know if someone has tried to deploy code built on their machine instead of code built by the server.

This approach does raise a small issue though – where do we keep the build number between builds?  i.e. how to we know what the previous build number was so we can increment it.  For this we’re going to use a simple version.txt file stored in the root of the drop folder.  If you want to secure this so that only the build process updates the version number I would suggest making the drop location read only for all users except the account the build runs under.

Anyway, enough talk.  Let’s see what we need to do.

Getting Started

1. Open VS2010 and create a solution with two C# Class LIbrary projects in it.  One for the custom build tasks we’re going to create, and one to hold the process template we’re going change.  I have called mine CustomBuildActivities and CustomBuildProcess

2. In the CustomBuildProcess project add a Workflow activity and then delete it.  This just helps with getting a bunch of references in place and adds the appropriate content type we’ll need when we add our build workflow in the next step.

image

3. Now go copy the DefaultTemplate.xaml file, rename it to something you like and include it in your project.  I’ve called mine VersionedBuildProcess.xaml.  Once it’s included in the solution change the Build Action to XamlAppDef.

image

At this point if you try compiling you’ll get a bunch of missing references, so add the following (sorry it’s a laundry list of items!)

  • System.Drawing
  • Microsoft.TeamFoundation.Build.Client
  • Microsoft.TeamFoundation.VersionControl.Client
  • Microsoft.TeamFoundation.WorkItemTracking.Client
  • %Program Files%\Microsoft Visual Studio 10.0\Common7\IDE\PrivateAssemblies\
    Microsoft.TeamFoundation.Build.Workflow.dll
  • %Program Files%\Microsoft Visual Studio 10.0\Common7\IDE\PrivateAssemblies\
    Microsoft.TeamFoundation.TestImpact.BuildIntegration.dll
  • %WinDir%\assembly\GAC_MSIL\Microsoft.TeamFoundation.TestImpact.Client
    \10.0.0.0__b03f5f7f11d50a3a\Microsoft.TeamFoundation.TestImpact.Client.dll

UPDATE: If you set the Build Action to None then you don't need to worry about setting all those references and you can still edit the XAML as per usual. This is a lot easier for many people, but the downside is that you can't verify the workflow is 100% correct unless you manually look through the whole workflow or try a build with it. My preference is the first, but do whatever you're comfortable with :-)

Create the First Custom Activity

Next  we’re going to create our first custom activity – this one will be the one we use to toggles the read only flags on AssemblyInfo files.  In your CustomBuildActivities project add a new Workflow Code Activity called SetReadOnlyFlag.  We’ll use this to toggle the read only bits for our AssemblyInfo files later on.

image

Now add references to Microsoft.TeamFoundation.Build.Client and Microsoft.TeamFoundation.VersionControl.Client and then use the following code for the class:

using System.Activities;
using Microsoft.TeamFoundation.Build.Client;
using Microsoft.TeamFoundation.VersionControl.Client;

namespace CustomActivities
{
[BuildActivity(HostEnvironmentOption.Agent)]
public sealed class SetReadOnlyFlag : CodeActivity
{
[RequiredArgument]
public InArgument<string> FileMask { get; set; }

[RequiredArgument]
public InArgument<bool> ReadOnlyFlagValue { get; set; }

[RequiredArgument]
public InArgument<Workspace> Workspace { get; set; }

protected override void Execute(CodeActivityContext context)
{
var fileMask = context.GetValue(FileMask);
var workspace = context.GetValue(Workspace);
var readOnlyFlagValue = context.GetValue(ReadOnlyFlagValue);

foreach (var folder in workspace.Folders)
{
foreach (var file in Directory.GetFiles(folder.LocalItem, fileMask, SearchOption.AllDirectories))
{
var attributes = File.GetAttributes(file);
if (readOnlyFlagValue)
File.SetAttributes(file, attributes | FileAttributes.ReadOnly);
else
File.SetAttributes(file,attributes & ~FileAttributes.ReadOnly);
}
}
}
}
}

The [BuildActivity] attribute on the class indicates that this class can be used as a workflow activity in the designer, and that it should be loaded on the build agent when a build runs.

This code here is how we define the arguments that the activity will use – they will appear in the properties window when the activity is selected in the designer, and the RequiredAttribute indicates that these properties must have values for the workflow to be valid.

[RequiredArgument]
public InArgument<string> FileMask { get; set; }

[RequiredArgument]
public InArgument<bool> ReadOnlyFlagValue { get; set; }

[RequiredArgument]
public InArgument<Workspace> Workspace { get; set; }

We’re going to use the Workspace to get information about where the build agent has physically put files on the disk so that we can get path information and search for the files we wish to change.  The other properties are used to actually find out what files to change and which way to set the flag.

Create The Second Custom Activity

For this activity we want to take a set of files as defined by a mask and do a search/replace on them to update the version numbers for the AssemblyVersion and AssemblyFileVersion attributes.

Here’s the code:

[BuildActivity(HostEnvironmentOption.Agent)]
public sealed class UpdateAssemblyVersionInfo : CodeActivity
{
[RequiredArgument]
public InArgument<string> AssemblyInfoFileMask { get; set; }

[RequiredArgument]
public InArgument<string> SourcesDirectory { get; set; }

[RequiredArgument]
public InArgument<string> VersionFilePath { get; set; }

[RequiredArgument]
public InArgument<string> VersionFileName { get; set; }

protected override void Execute(CodeActivityContext context)
{
var sourcesDirectory = context.GetValue(SourcesDirectory);
var assemblyInfoFileMask = context.GetValue(AssemblyInfoFileMask);
var versionFile = context.GetValue(VersionFilePath) + @"\" + context.GetValue(VersionFileName);

//Load the version info into memory
var versionText = "1.0.0.0";
if (File.Exists(versionFile))
versionText = File.ReadAllText(versionFile);
var currentVersion = new Version(versionText);
var newVersion = new Version(currentVersion.Major, currentVersion.Minor, currentVersion.Build + 1, currentVersion.Revision);
File.WriteAllText(versionFile, newVersion.ToString());

bool changedContents;
foreach (var file in Directory.EnumerateFiles(sourcesDirectory, assemblyInfoFileMask, SearchOption.AllDirectories))
{
var text = File.ReadAllText(file);
changedContents = false;
// we want to find 'AssemblyVersion("1.0.0.0")' etc
foreach (var attribute in new[] { "AssemblyVersion", "AssemblyFileVersion" })
{
var regex = new Regex(attribute + @"\(""\d+\.\d+\.\d+\.\d+""\)");
var match = regex.Match(text);
if (!match.Success) continue;
text = regex.Replace(text, attribute + "(\"" + newVersion + "\")");
changedContents = true;
}
if (changedContents)
File.WriteAllText(file, text);
}
}
}

At this point you should be able to compile your application and see it all pass, but until we change the workflow itself this is pretty useless.

Updating the Build Workflow

In your process project open up the build process.  In the toolbox you should now see something like this:

image

We want to drop these tasks into our workflow at the appropriate point, so to do this find the part of the workflow that looks something like this:

image

For reference it’s in ….Sequence > Run On Agent > Initialize Workspace (near the bottom)

Now add a sequence activity after the get workspace activity like so:

image

Change the display name property to “Update AssemblyInfo Versions"” or something similar.  This not only appears in the build log, but has the advantage of helping you understand your workflow better.

Now drag the ReadOnly flag activity we created into the new sequence activity you added – it should now look something like this:

image

That error indicator means we haven’t yet supplied some property values for the activity, so we’d best do that now.

Set Activity Properties

If you look at the properties window for our activity you should see something like this:

image

What we want is for the users of our workflow to be able to specify the values for the FileMask via the Build Definition window, and we want to set the values for the Workspace based on whatever is current in our workflow.

Let’s tackle the FileMask first.  To do this we’re going to add a new Argument to our overall workflow.  Find the Arguments tab at the bottom of the designer:

image

Click in the Create Argument box and add a new argument called AssemblyInfoMask and give it a default value of “AssemblyInfo.*”

Before we use this in our activities let’s just make a few more changes to make this argument appear in the build process window the way we want it.  Go to the metadata argument a few lines above and click the ellipsis […] button.  You will see a window appear where we can set values that will let Visual Studio know how to display this argument to the end user.  Let’s do just that:

image

Now let’s fix the properties on our Activity

image

Now you may be asking where the Workspace value comes from since it’s not an argument for the workflow.  If you look a few activities above you will see a create workspace activity and one of it’s properties is an OutProperty called Result:

image

This is where the Workspace variable is created.  Unfortunately there’s not really an easy way of tracking this sort of thing with the workflow, but fortunately most variables in the workflow are well named and you should be able to figure out what most of them do.

Finishing the Workflow

Let’s finish off the workflow.  Drag the UpdateAssemblyVersionInfo task into the workflow along with a second SetReadOnlyFlag task.

Before we set the properties on these, we’re going to need one more workflow Argument, being the name of the version file we’re going to store the version information in.

image

Don’t forget to set the Metadata as well so that it appears in the correct place in Visual Studio when we define builds.

Now set the properties on the UpdateAssemblyVersionInfo activity as follows:

image

Since we want to store the file we hold our version info at the root of the drop location we have set the VersionFilePath to BuildDetail.DropLocationRoot.  You can of course, change this to put it wherever you like.

Finally set the properties for the second SetReadOnlyFlag task, this time with the ReadOnly flag set to true.

At this point build your project.  If it’s all OK everything should compile and you should be ready for the next step.

Using The New Build Process

Before we can use the new build process, we have to ensure both the process and the custom activities are in source control.

The .xaml process file should be added to the BuildProcessTemplates folder, and I usually place custom activities in a subfolder within that.  For example:

image

Now we have to tell our Build Controller where to find our custom activities otherwise the build will fail.  Go to Manage Build Controllers…

image

and in the properties window set the path to the activities

image

Now we can finally create a build definition.

In the new build definition window, go to the Process tab, select the Show Details window and Click New..

image

Select an existing .xaml file (being the new process you uploaded)

image

If you set your metadata properties correctly, the process settings should now look something like this:

image

If all goes well, you should get a completed build and see something like the following in the log, indicating our new steps occurred.

image

Finally go to the drop location, right click one of your assemblies and see that the version information is being updated.

Phew! That’s it!  We’re done!

I know it’s a fair bit of work getting this all set up, but once you’ve done it once and have your initial workflow project in place, you can use it as the basis for any future process customisations which makes things much quicker.  Also as you get more familiar with the workflows themselves and knowing where to make changes the process customisation itself will also become faster and easier.

Good luck customising your builds!