Jul 30, 2010

How To: Versioning Builds With TFS 2010

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!

26 comments:

  1. Any thoughts on how we can make use of all the libraries that were written for MSBuild in the days of TFS 200X? It rather feels like reinventing the wheel to come up with all these tasks!

    ReplyDelete
  2. Do you know of a Hot-to post on TFS installation. I installed TFS back in march with the RC version, I all was working fine. When I got the new version I tried to re-install TFS but somehow my Sharepoint and Report site don't work properly and I am still strugling with making them work.

    Anyway...

    What an awesome post. It was verry informative to follow your instruction. Looking forward to new How-to's

    ReplyDelete
  3. 3 Now go copy the DefaultTemplate.xaml file

    Umm file does not exist .. btw for a step by step finding some things are very vague

    ReplyDelete
  4. @Anonymous The DefaultTemplate.xaml file should be in your build process templates folder, in the top level of your projects source control folder. ie $/YourProject/BuildProcessTemplates

    ReplyDelete
  5. Very nice! Thanks for laying it out so clearly.

    I would have been nice to use a Argument for the VersionFilePath. But BuildDetail is not accessible as a Value of an argument.

    Why is that? Any way to get it (or something like it) at that point?

    ReplyDelete
  6. Hello, thanks for this post, it helped me a lot;) Well, let me point out you a possible issue in SetReadOnlyFlag activity in the line 27: If a workspace folder is a Cloaked folder, the folder.LocalItem has a null value (because the folder has no local mapping), and the Directory.GetFiles throws an ArgumentNullException due to its first path parameter is null. I've just put the "If (folder.IsCloaked) continue;" right before the line 27 and the code works like a charm :)

    regards,
    attila

    ReplyDelete
  7. @Atilla Thanks for pointing out a bug and for providing the fix as well! :-)

    ReplyDelete
  8. This article has been very useful and an approach we would like to use although we would like the version.txt file to be under source control along side the solution. The only issue with this is where we branch Main to the Release. The Version.txt file would be branched. This could lead to the accidental merging of the Release version of the file back to Main. One thought is to have a version.xml at the root of the project. Higher than the branches. This could contain a list of solution file paths along with the current version. This would be checked out, changed and checked back in. The thing I am struggling with is grabbing a file which is outside the Workspace defined in the build definition.

    ReplyDelete
  9. Thanks for this really good howto. It works like a charm and is easy to maintain and/or extend.

    ReplyDelete
  10. I was able to get everything to build properly, but am unable to get the display of the "Versioning" in the Build Definition. Moved the .dlls to the CustomActivities under the BuildProcessTypes and put the newly modified .xaml file in the, pointed the Controller to the CustomActivies. Can anyone offer more details of what should be where if this does not work? Which projects should be built and defined in the build definition? Thanks.

    ReplyDelete
  11. Richard,
    Works like a charm. Was wondering how if you have multiple projects in a collection, with each having their own ProcessTemplate folder, how can you set the BuildController to each and everyone of these builds. Assume that each project may want to sequence their builds a little different within the TFC.
    --chk

    ReplyDelete
  12. this is unbelievable! something nearly every person who begins using TFS will eventually want to do, and it isn't a built in function of the build definition. Thank you Microsoft, for again releasing a great product - in theory only -leaving it lacking basic (read easily usable) functionality.

    ReplyDelete
  13. Why I receive this message: "Cannot create unknown type '{clr-namespace:CustomBuildActivities;assembly=CustomBuildActivities}SetReadOnlyFlag'."

    any idea ??
    tks

    ReplyDelete
  14. Im gettting same exact error: TF215097:

    An error occurred while initializing a build for build definition \Manitou\DBManager: Cannot create unknown type '{clr-namespace:CustomBuildActivities;assembly=CustomBuildActivities}SetReadOnlyFlag'.

    ReplyDelete
  15. Cannot create unknown type '{clr-namespace:CustomBuildActivities.CustomActivities;assembly=CustomBuildActivities}SetReadOnlyFlag'.

    ?? not working :(

    ReplyDelete
  16. i have completed all the process and while running the build, in the Update Assembly Versions step, i got the below error:

    Update Assembly VersionInfo
    "Input string was not in a correct format."

    I have given my argument type as string properly.. i am getting this error and i couldnt find the solution. Even i have tried with doublequotes and without doublequotes in the edit build definition window.

    Please help.

    ReplyDelete
  17. Helping @Anonymous offline to try and diagnose the problem

    ReplyDelete
  18. Thanks Richard, this was a big help. I adapted it as I needed assemblies to retain most of their own version numbers but needed the build to replace the revision part of each assembly version with the latest Changeset number.

    I pass the Changeset number to UpdateAssemblyVersionInfo using BuildDetail.SourceGetVersion from there it's just a bit of string manipulation to get the rest of the job done.

    I'm happy to share my code but don't know how best to post it here so let me know if you want a copy.

    ReplyDelete
  19. In step: "Updating the Build Workflow" mine is just an empty screen with no steps. Feels a bit like we missed something somewhere along the way.

    ReplyDelete
  20. Thanks for this post, it was very helpful!

    For those having trouble with:
    Cannot create unknown type '{clr-namespace:CustomBuildActivities.CustomActivities;assembly=CustomBuildActivities}SetReadOnlyFlag'.

    You need to check in the binaries for the task, and then tell the buildcontrollers where in TFS the binaries are located. Passing the project path alone is not sufficient.

    ReplyDelete
  21. My version.txt file shows the 3rd field being incremented, yet the parent document (tfs 2008) says the 4th field will be incremented. Our processes use the first three fields, therefore we must have the 4th field incremented. Any help with that? Also, the product version field of the dll built is blank. I get no errors during the build. Why is no product version created in the dll properties?

    ReplyDelete
  22. Regarding "Cannot create unknown type '{clr-namespace:CustomBuildActivities.CustomActivities;assembly=CustomBuildActivities}SetReadOnlyFlag'"

    I was having a very similar problem. However, checking in the assemblies (the binaries for the task) did not help. The reason was that I was creative and lazy enough to make 2 separate projects, so I decided to put both the XAML and the activities to the same project. As the result, the XAML was missing the ";assembly=" part of the xmlns reference. I opened the XAML in Notepad and fixed the issue manually.

    xmlns:local="clr-namespace:CustomBuildActivities;assembly=CustomBuildActivities"

    ReplyDelete
  23. Hi Richard i am trying to set Major and Minor version from build defination. I have 2 in parameter both as string still i am getting Input string was not in a correct format

    ReplyDelete
  24. Richard,
    Some questions... Why is the namespace CustomActivities and not CustomBuildActivities to match the project name? I know it doesn't have to be identical, just curious.

    Does the name of the folder in SourceControl have to match the namespace? Instead of CustomActivities, since I kept my namespace CustomBuildActivities do I have to keep the name of the folder the same?

    At the point where you put everything in Source Control, I have had my work already in SC from the moment I made the solution & projects. So, when you state to "place custom activities in a subfolder..." are you talking about the *.cs files, the entire CustomBuildActivities project, or the resulting *.dlls??

    Just some clarification would be most helpful. I do appreciate the article. It is helping, just fuzzy in some areas. :)

    -Scott.

    ReplyDelete
  25. Many thanks, worked with a bit of tweaking for TFS 12

    Needed these reference too

    Microsoft.TeamFoundation.Common
    Microsoft.TeamFoundation.VersionControl.Common

    Otherwise perfect, I learn't a lot from this thanks :-)

    ReplyDelete
  26. I tried the steps listed for VS2012. However, when I am trying to use it in my build definition, i land up with one error:
    TF215097: An error occurred while initializing a build for build definition \HomeCrossDevice\McAfeeBuildTemplate:
    Exception Message: Expression of type 'Microsoft.TeamFoundation.Build.Workflow.Activities.BuildSettings' cannot be used for return type 'Microsoft.TeamFoundation.Build.Workflow.Activities.BuildSettings' (type ArgumentException)
    Exception Stack Trace: at System.Linq.Expressions.Expression.ValidateLambdaArgs(Type delegateType, Expression& body, ReadOnlyCollection`1 parameters)
    at System.Linq.Expressions.Expression.Lambda[TDelegate](Expression body, String name, Boolean tailCall, IEnumerable`1 parameters)
    at System.Linq.Expressions.Expression.Lambda[TDelegate](Expression body, Boolean tailCall, IEnumerable`1 parameters)
    at Microsoft.VisualBasic.Activities.VisualBasicHelper.Compile[T](LocationReferenceEnvironment environment, Boolean isLocationReference)
    at Microsoft.VisualBasic.Activities.VisualBasicHelper.Compile[T](CodeActivityPublicEnvironmentAccessor publicAccessor, Boolean isLocationReference)
    at Microsoft.VisualBasic.Activities.VisualBasicHelper.Compile[T](String expressionText, CodeActivityPublicEnvironmentAccessor publicAccessor, Boolean isLocationExpression)
    at Microsoft.VisualBasic.Activities.VisualBasicValue`1.CacheMetadata(CodeActivityMetadata metadata)
    at System.Activities.CodeActivity`1.OnInternalCacheMetadataExceptResult(Boolean createEmptyBindings)
    at System.Activities.Activity`1.OnInternalCacheMetadata(Boolean createEmptyBindings)
    at System.Activities.Activity.InternalCacheMetadata(Boolean createEmptyBindings, IList`1& validationErrors)
    at System.Activities.ActivityUtilities.ProcessActivity(ChildActivity childActivity, ChildActivity& nextActivity, Stack`1& activitiesRemaining, ActivityCallStack parentChain, IList`1& validationErrors, ProcessActivityTreeOptions options, ProcessActivityCallback callback)
    at System.Activities.ActivityUtilities.ProcessActivityTreeCore(ChildActivity currentActivity, ActivityCallStack parentChain, ProcessActivityTreeOptions options, ProcessActivityCallback callback, IList`1& validationErrors)
    at System.Activities.ActivityUtilities.CacheRootMetadata(Activity activity, LocationReferenceEnvironment hostEnvironment, ProcessActivityTreeOptions options, ProcessActivityCallback callback, IList`1& validationErrors)
    at System.Activities.Validation.ActivityValidationServices.InternalActivityValidationServices.InternalValidate()
    at Microsoft.TeamFoundation.Build.Workflow.WorkflowHelpers.ValidateWorkflow(Activity activity, ValidationSettings validationSettings)
    at Microsoft.TeamFoundation.Build.Hosting.BuildProcessCache.LoadFromXaml(String workflowXaml, TextExpressionImports textExpressionImports)
    at Microsoft.TeamFoundation.Build.Hosting.BuildControllerWorkflowManager.PrepareRequestForBuild(WorkflowManagerActivity activity, IBuildDetail build, WorkflowRequest request, IDictionary`2 dataContext)
    at Microsoft.TeamFoundation.Build.Hosting.BuildWorkflowManager.TryStartWorkflow(WorkflowRequest request, WorkflowManagerActivity activity, BuildWorkflowInstance& workflowInstance, Exception& error, Boolean& syncLockTaken)

    ReplyDelete