Aligning Build Numbers with Assembly Versions in TFS2008.

| 13 Comments

I like my build numbers to be the same number that my assemblies are versioned with (and my end deliverables).  It just makes things easier to track, that way if I get a bug report in from a customer I can look at the version and easily look at the label in source control to see what code that included. In all deliverables provided to the customer, we always output the version obtained from the current assembly somewhere at the start of any diagnostic information, that way you can easily tell what version they are on and instantly track this back.  This all helps to make it easy for bugs to be filed against the correct version and reported in which version they have been fixed (using the nice integration between Team Build and the Work Item tracking portion of TFS).

People are often surprised that this feature does not work "out the box" with Team Build, so I thought I would just take the time to document how I made this work for us internally.  As you'll be able to see, in TFS2008 all the basic hooks are provided for us to support this way of working.

Firstly, our .NET version numbering uses a slightly different scheme to our Java version numbering.  In our Java products, the "build number" portion of the version number is actually the changeset number of TFS at that point in time.  In .NET there are 4 components to a typical assembly version number (1.0.1234.5678) and the maximum value for each number is 65535.  Our production TFS server is currently at changeset 7698 which means that we would get about 6 years out of such a build numbering scheme for .NET - that would be perfectly satisfactory if you had a changeset epoch after each major release (so you would reset the build number to be current changeset - 7698 if we did a major version today).  However Team Build needs a unique name for each build - using a changeset based approach risks having two builds with the same build number.  So rather than do a changeset based system, I decided to make the .NET build numbers be a straight-forward incrementing number. I rely of the default functionality of Team Build to create a label for that build number to track the number back to version control.  The incrementing number value is stored in a file on the default drop location for the build.

Another thing that I should explain is that I don't personally like the "standard" Microsoft way of versioning assemblies as:-

<Major>.<Minor>.<Build>.<Service>

To me, it reads much easier as:-

<Major>.<Minor>.<Service>.<Build>

Where <Build> is the number that increments every time a build is performed.  As far as I am concerned, this difference is mostly cosmetic as it doesn't change the way the CLR resolves the assembly versions, however feel free to correct me in the comments if I am talking rubbish.

So - onto how we accomplish this.  Firstly, in TFS2008 there is a convenient target for you to override to generate your custom build numbers called "BuildNumberOverrideTarget".  The important thing is that each build number must be unique, therefore a good rule of thumb is to use something like BuildDefinitionName_1.0.0.1234.  Inside the BuildNumberOverrideTarget you simply set "BuildNumber" property to be what you want.  Here is ours:-

<PropertyGroup> 
  <VersionMajor>1</VersionMajor> 
  <VersionMinor>0</VersionMinor> 
  <VersionService>0</VersionService> 
  <VersionBuild>0</VersionBuild> 
</PropertyGroup>
<Target Name="BuildNumberOverrideTarget"> 
  <!-- Create a custom build number, matching the assembly version -->      
  <Message Text="Loading last build number from file &quot;$(DropLocation)\buildnumber.txt&quot;" /> 
  <IncrementingNumber NumberFile="$(DropLocation)\buildnumber.txt"> 
    <Output TaskParameter="NextNumber" PropertyName="VersionBuild" /> 
  </IncrementingNumber> 
  <PropertyGroup> 
    <BuildNumber>$(BuildDefinitionName)_$(VersionMajor).$(VersionMinor).$(VersionService).$(VersionBuild)</BuildNumber> 
  </PropertyGroup> 
  <Message Text="Build number set to &quot;$(BuildNumber)&quot;" />  
</Target>

The first thing I do is call a quick custom task I wrote that increments the build number stored in the passed file.  I wanted to do this while keeping a lock on the file itself in case two builds tried to update the same file at the same time.  We then take this new number and build the BuildNumber based upon that value.  The code for the Incrementing Number task is very simple and is given below:-

using System; 
using System.IO; 
using Microsoft.Build.Framework; 
using Microsoft.Build.Utilities; 

namespace Teamprise.Tasks
{
/// <summary>
/// A simple task to increment the number stored in a passed file.
/// </summary>
public class IncrementingNumber : Task
{
public override bool Execute()
{
NextNumber = IncrementNumber();
return true;
}
public int IncrementNumber()
{
using (FileStream fs = new FileStream(NumberFile, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None))
{
StreamReader reader = new StreamReader(fs);

long pos = 0;
String line = reader.ReadLine();

// Ignore comments in file
while (line != null && line.StartsWith("#"))
{
pos = pos + line.Length + System.Environment.NewLine.Length;
line = reader.ReadLine();
}

int number = -1;
if (line != null)
{
number = Int32.Parse(line);
}
NextNumber = number + 1;

// Rewind the file stream back to the beginning of the number part.
fs.Position = pos;

StreamWriter writer = new StreamWriter(fs);
writer.WriteLine(NextNumber.ToString());
writer.Flush();
writer.Close();
}
return NextNumber;
}
[Required]
public string NumberFile { get; set; }
[Output]
public int NextNumber { get; set; }
}
}


You compile this code into an assembly of your choice that lives alongside the TFSBuild.proj file in the build configuration folder in source control and is this loaded using the UsingTask call at the begging of your MSBuild project, i.e.

<UsingTask TaskName="Teamprise.Tasks.IncrementingNumber" 
           AssemblyFile="Teamprise.Tasks.dll" />

The next thing that we have to do is to take the new version and force this into the assemblyinfo files.  Personally, I prefer the AssemblyInfo files stored in source control to have a certain well defined number for each release branch (i.e. 1.0.0.0), and make it the build server that versions them.  Some people like to check these back into source control - if you do that, be sure to check them in with the special comment of "***NO_CI***" to ensure that the check-in does not trigger any CI builds potentially putting you into an infinite loop of building.

So, we modify our assembly version files after they have been downloaded from source control using a technique borrowed from Richard Banks, our interpretation of this is given below:-

<ItemGroup> 
  <AssemblyInfoFiles Include="$(SolutionRoot)\**\assemblyinfo.cs" /> 
</ItemGroup>   
<Target Name="AfterGet"> 
  <!-- Update all the assembly info files with generated version info --> 
  <Message Text="Modifying AssemblyInfo files under &quot;$(SolutionRoot)&quot;." /> 
  <Attrib Files="@(AssemblyInfoFiles)" Normal="true" /> 
  <FileUpdate Files="@(AssemblyInfoFiles)"                                 
              Regex="AssemblyVersion\(&quot;.*&quot;\)\]"                 
              ReplacementText="AssemblyVersion(&quot;$(VersionMajor).$(VersionMinor).$(VersionService).$(VersionBuild)&quot;)]" /> 
  <FileUpdate Files="@(AssemblyInfoFiles)" 
              Regex="AssemblyFileVersion\(&quot;.*&quot;\)\]" 
              ReplacementText="AssemblyFileVersion(&quot;$(VersionMajor).$(VersionMinor).$(VersionService).$(VersionBuild)&quot;)]" /> 
  <Message Text="AssemblyInfo files updated to version &quot;$(VersionMajor).$(VersionMinor).$(VersionService).$(VersionBuild)&quot;" /> 
</Target>

As you can see, we are making use of the custom Attrib task that is provided by the essential MSBuild Community Tasks to set the files to read/write and then we are calling the MSBuild Community Task FileUpdate to do a couple of regular expression search replaces on the appropriate parts of the files.

And that's about all that needs to be done.  Now our builds have nice incrementing numbers that have the version number included that is the same as the assembly info files.

13 Comments

Hi Richard,

This is excellent and exactly what i've been looking for as we used to do this process with NAnt and now are migrating to TF Build.

A couple of questions:

You don't checkout or checkin the buildnumber.txt file. So where does this live? In the $(DropLocation), but where is this?

My buildnumber.txt sits alongside my TFSBuild.proj file and i have to check it out in the BuildNumberOverrideTarget, however this is causing me problems as this is called before the 'BeforeGet' or any of the get targets so the file is not actually there!

Cheers,
Andy

The Drop location is the network folder than all the builds get copied to when they have finished being built ready for staging. Therefore the buildnumber.txt is actually not under version control.

However I am not completely happy with this scenario.

If you store your buildnumber.txt file alongside the TFSBuild.proj file then Team Build will actually get that file for you as part of the bootstrap process. It actually downloads everything in folder that the TFSBuild.proj file lives (but doesn't download things in sub-folder by default). Therefore you will have it in BeforeGet.

You will at some point obviously want to check this file in at some point (probably in BeforeGet or AfterGet). When you do this check-in (by doing an exec of the TFS command line "tf /checkin", but sure to check-in with a comment of "***NO_CI***" to ensure that the check-in didn't trigger another CI build which would put you in a recursive build loop.

Hope that helps,

Martin.

Hi,

I completely agree on using an incremented number at the end of the version number! But how should it be called to avoid confusion with TFS's whole "build number"? I'll go with "revision suffix" for the rest of this comment...

So, with the same idea in mind, our approach to this was slightly different. In BuildNumberOverrideTarget, we call a custom task just like you do, except that it doesn't read the revision suffix from a file and write it back. Instead it queries the BuildStore, gets the biggest existing suffix for that build definition, and adds 1.

A loop on the existing build data can give us this information quite easily:

------------------------------------------
using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.Build.Proxy;
...
TeamFoundationServer server = TeamFoundationServerFactory.GetServer(this.TfsUrl);
BuildStore store = (BuildStore)server.GetService(typeof(BuildStore));
BuildData[] buildList = store.GetListOfBuilds(this.TfsProject, this.TfsBuildType);
foreach (BuildData data in buildList)
{
   // just find the biggest revision suffix in use
   // by looking at the end of each data.BuildNumber
}
------------------------------------------

That's it we have the new revision suffix, and we can use it when overriding the build number.

What do you think about this approach?
We haven't been using it for long but it seems to work fine for the moment.

Cheers

Romain


Hi I successfully implemented the concept of incrementing revision number but I am not able to checkin the Files.

I am using following commands

Command=""C:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE\TF.exe" checkin /comment:"$(NoCICheckInComment) " /noprompt /override:"Testing" /notes:"Code Reviewer"="abbagchi" /recursive "C:\BLD1\Sources\TestProject\TestProject\Properties\assemblyinfo.cs""/>


but the TF.exe command failed and exites with 100 code

Please help me

Personally I do not like the build process to check in files that it modifies. However - to figure out what is going wrong in your case I would take a look at the BuildLog.txt from the build as that will contain the error message from tf.exe

Good luck,

Martin.

sorry I didn't stick in the actual error message it is, the error I get is
error MSB4067: The element beneath element is unrecognized.

I could be to do with the version of TeamBuild i.e. I'm using 2005.

sorry I'll try this again, the TeamBuild will not pick up the BuildNumber property.

here's the error
error MSB4067: The element @BuildNumber@ beneath element @PropertyGroup@ is unrecognized.

I'm using TeamBuild 2005.

I'm implementing this on TFS 2008 but I'm getting error MSB4036: The "Attrib" task was not found. Thanks in advance for your help. Below is the tail end of the BuildLog.txt:

------------------------------------------
Done building target "CoreGet" in project "TFSBuild.proj".
Target "AfterGet" in file "C:\Documents and Settings\svc-alv-tfs-svc\Local Settings\Temp\POC_TeamBuild_WDP\POC\BuildType\TFSBuild.proj" from project "C:\Documents and Settings\svc-alv-tfs-svc\Local Settings\Temp\POC_TeamBuild_WDP\POC\BuildType\TFSBuild.proj":
Task "Message"
  Modifying AssemblyInfo files under "C:\Documents and Settings\svc-alv-tfs-svc\Local Settings\Temp\POC_TeamBuild_WDP\POC\Sources".
Done executing task "Message".
C:\Documents and Settings\svc-alv-tfs-svc\Local Settings\Temp\POC_TeamBuild_WDP\POC\BuildType\TFSBuild.proj(252,5): error MSB4036: The "Attrib" task was not found. Check the following: 1.) The name of the task in the project file is the same as the name of the task class. 2.) The task class is "public" and implements the Microsoft.Build.Framework.ITask interface. 3.) The task is correctly declared with  in the project file, or in the *.tasks files located in the "C:\WINDOWS\Microsoft.NET\Framework\v3.5" directory.
Done building target "AfterGet" in project "TFSBuild.proj" -- FAILED.
Done Building Project "C:\Documents and Settings\svc-alv-tfs-svc\Local Settings\Temp\POC_TeamBuild_WDP\POC\BuildType\TFSBuild.proj" (EndToEndIteration target(s)) -- FAILED.

Build FAILED.

"C:\Documents and Settings\svc-alv-tfs-svc\Local Settings\Temp\POC_TeamBuild_WDP\POC\BuildType\TFSBuild.proj" (EndToEndIteration target) (1) ->
(AfterGet target) ->
C:\Documents and Settings\svc-alv-tfs-svc\Local Settings\Temp\POC_TeamBuild_WDP\POC\BuildType\TFSBuild.proj(252,5): error MSB4036: The "Attrib" task was not found. Check the following: 1.) The name of the task in the project file is the same as the name of the task class. 2.) The task class is "public" and implements the Microsoft.Build.Framework.ITask interface. 3.) The task is correctly declared with in the project file, or in the *.tasks files located in the "C:\WINDOWS\Microsoft.NET\Framework\v3.5" directory.

0 Warning(s)
1 Error(s)

Time Elapsed 00:00:01.96
------------------------------------------


The text from the log I posted was cut off. Here's another try...

error MSB4036: The "Attrib" task was not found.
Check the following:
1.) The name of the task in the project file is the same as the name of the task class.
2.) The task class is "public" and implements the Microsoft.Build.Framework.ITask interface.
3.) The task is correctly declared with in the project file, or in the *.tasks files located in the "C:\WINDOWS\Microsoft.NET\Framework\v3.5" directory.

Figured it out...

I missed the part about installing MSBuild Community Tasks. All is well after I installed it and added this to TFSBuild.proj:

Attrib is part of the MSBuild Community Tasks project...

Oops, hadn't refreshed yet today when I replied there... missed your post right before mine.

Done executing task "Get".
Task "SetBuildProperties" skipped, due to false condition; ( '$(GetVersion)' != '$(SourceGetVersion)' ) was evaluated as ( 'C87748' != 'C87748' ).
Done building target "CoreGet" in project "TFSBuild.proj".
Target "AfterGet" in file "C:\MyAccount_Build\BuildType\TFSBuild.proj" from project "C:\MyAccount_Build\BuildType\TFSBuild.proj":
Task "Message"
Modifying AssemblyInfo files under 'C:\MyAccount_Build\Sources' and 'C:\MyAccount_Build\BuildType'.
Done executing task "Message".
Using "Attrib" task from assembly "C:\Program Files\MSBuild\MSBuildCommunityTasks\MSBuild.Community.Tasks.dll".
Task "Attrib"
Done executing task "Attrib".
Using "FileUpdate" task from assembly "C:\Program Files\MSBuild\MSBuildCommunityTasks\MSBuild.Community.Tasks.dll".
Task "FileUpdate"
C:\MyAccount_Build\BuildType\TFSBuild.proj : error : Object reference not set to an instance of an object.
Done executing task "FileUpdate" -- FAILED.
Done building target "AfterGet" in project "TFSBuild.proj" -- FAILED.
Done Building Project "C:\MyAccount_Build\BuildType\TFSBuild.proj" (EndToEndIteration target(s)) -- FAILED.

Build FAILED.
Can someone let me know where I went wrong.

Leave a comment

Archives

Creative Commons License
This blog is licensed under a Creative Commons License.