Build Identifier Tasks
This post is a follow-up to “Builds: how many?” and drills down how to compute the version number during the build. I assume knowledge of customizing the build identifier: you do not read the docs: “How to: Customize Build Numbers” for TFS 2008 and “Customize Build Numbers” for TFS 2010.
The approach I found more frequently applied, is to leverage a file, which may or may be not kept under version control. For example read “How to AutoIncrement version with each build using Team Foundation Server build (with a little help from AssemblyInfoTask)” and “Aligning Build Numbers with Assembly Versions in TFS2008” or “Matching TFS build labels with custom build number”. I recent post on this subject is “Incrementing the Assembly Version for Each Build”.
I don’t like this approach for two reasons:
- if the file is not under version control, you may loose its content, or use some wrong data; when you have multiple build servers it has to reside on a network share;
- if the file is under version control, your build needs to check it in causing all those spurious check-ins; they shows up in all kind of reports and it is an unneeded noise.
What I like more is leveraging as much as possible information at hand and TFS is plenty of that: it already keeps track of your builds — who started them, when, what code has been used, the build results, and so on.
So introduce a less know property:
LastBuildNumber
This property, as you may guess, contains the last build identifier, scoped to the build definition executing. So if you queue a new build for the build definition named Daily, $(LastBuildNumber) will evaluate Daily_20100506.
I wrote, time ago, a custom task for parsing this property and generating a new build identifier to use in the BuildNumberOverrideTarget target. This is a simplified version:
1: if (!string.IsNullOrEmpty(LastBuildNumber))
2: {
3: // parse input
4: string[] nameParts = LastBuildNumber.Split(Separator);
5: if (nameParts.Length > 1)
6: {
7: string[] nums = nameParts[1].Split('.');
8: if (nums.Length > 2)
9: {
10: major = int.Parse(nums[0]);
11: minor = int.Parse(nums[1]);
12: build = int.Parse(nums[2]);
13: revision = int.Parse(nums[3]);
14: }//if
15: }//if
16: // increment
17: if (IncrementBuild)
18: build++;
19: if (IncrementRevision)
20: revision++;
21: }//if
22: // recompose
23: resultingBuildNumber = string.Format("{0}{1}{2}.{3}.{4}.{5}", BuildDefinitionName, Separator, major, minor, build, revision);
This code may be packed either in a MSBuild Task for TFS 2008 or in a Workflow activity in 2010 (see also “Generating Custom Build Numbers in TFS Build 2010”).
Up to now, I have not been so original, as other wrote about using LastBuildNumber, e.g. “Custom Build Numbers in Team Build”. The point here is that the only responsibility for the custom task or activity is to parse, increment and reassemble.
Reusing an identifier
Using LastBuildNumber is nice as long as things goes well, but sometimes a build breaks before reaching BuildNumberOverrideTarget. The ugly consequences? Chances are that the next build will broke again with «The build number {0} already exists. Please ensure that the build number generated is unique for the team project».
It’s easy to fix such issue: just pass the value on the command line, in the Queue Build dialog or to the TFSBuild tool, like this:
/p:LastBuildNumber=Daily_1.0.99.0
Remember that properties passed on the command line always take precedence.
Querying the build database
While LastBuildNumber is nice, if you need to use sophisticated logic for creating build number, you may tap into the TFS Build database to get all information about previous and running builds.
Recently I came back to this approach in order to
- increment the same version number from different build definitions
- pick different version numbers in different branches
It works in three steps: query the build database for all build of the current TeamProject, filter the result and get the more recent ‘valid’ build. The sample source code follows.
1: public override bool Execute()
2: {
3: Log.LogMessage(MessageImportance.Normal, "Querying Build database");
4: //flatten
5: var defs = from d in this.SourceBuildDefinitions
6: select d.ItemSpec;
7: Log.LogMessage(MessageImportance.Low, " BuildDefinition filter is {0}", defs.Aggregate("", (acc, x) => acc + " " + x));
8: string[] branches = new string[0];
9: if (this.Branches != null && this.Branches.Length > 0)
10: {
11: branches = (from b in this.Branches select b.ItemSpec).ToArray();
12: Log.LogMessage(MessageImportance.Low, " Branch filter is {0}", branches.Aggregate("", (acc, x) => acc + " " + x));
13: }
14: else
15: {
16: Log.LogMessage(MessageImportance.Low, " No branch filter");
17: }
18: TeamFoundationServer tfs = TeamFoundationServerFactory.GetServer(TeamFoundationServerUrl);
19: IBuildServer server = (IBuildServer)tfs.GetService(typeof(IBuildServer));
20: var query = server.CreateBuildDetailSpec(TeamProject);
21: query.MaxBuildsPerDefinition = HistoryDepth;
22: query.QueryOrder = BuildQueryOrder.FinishTimeDescending;
23: query.QueryOptions = QueryOptions.Definitions;
24: var allBuilds = from b in server.QueryBuilds(query).Builds
25: select new
26: {
27: Number = b.BuildNumber,
28: DefinitionName = b.BuildDefinition.Name,
29: SortableNumber = ConvertToSortableBuildNumber(b.BuildNumber),
30: ConfigurationFolderPath = GetConfigurationFolderPath(b),
31: Status = b.Status,
32: FinishTime = b.FinishTime,
33: BuildFinished = b.BuildFinished
34: };
35: var basic = from b in allBuilds
36: where defs.Contains(b.DefinitionName)
37: let Id = b.SortableNumber
38: let parts = b.ConfigurationFolderPath.Split('/')
39: let containerName = parts[2]
40: let isTrunk = (string.Compare(containerName, TrunkName, true) == 0)
41: let Branch = isTrunk ? containerName : containerName + '/' + parts[3]
42: orderby Id descending
43: select new
44: {
45: Id, b.Number,
46: DefinitionName = b.DefinitionName,
47: Branch, b.FinishTime, b.BuildFinished,
48: b.Status, b.ConfigurationFolderPath
49: };
50: var filter = (branches.Length > 0)
51: ? from b in basic
52: where branches.Contains(b.Branch)
53: select b
54: : basic;
55: string pickedBranch = "<none>";
56: var pick = filter.FirstOrDefault();
57: if (pick != null)
58: {
59: lastUsed = pick.Number;
60: pickedBranch = pick.Branch;
61: }//if
62: Log.LogMessage(MessageImportance.Normal, "Picked build number {0} for {1} branch", lastUsed, pickedBranch);
63:
64: return true;
65: }
66:
67: private string ConvertToSortableBuildNumber(string buildNumber)
68: {
69: string numPart = buildNumber.Split(Separator)[1];
70: var numbers = numPart.Split('.');
71: int major = 0;
72: int minor = 0;
73: int build = 0;
74: int revision = 0;
75: if (numbers.Length > 3)
76: {
77: major = int.Parse(numbers[0]);
78: minor = int.Parse(numbers[1]);
79: build = int.Parse(numbers[2]);
80: revision = int.Parse(numbers[3]);
81: }
82: else
83: {
84: build = int.Parse(numbers[0]);
85: revision = int.Parse(numbers[1]);
86: }
87: return string.Format("{0:D4}.{1:D4}.{2:D8}.{3:D4}", major, minor, build, revision);
88: }
89:
90: private string GetConfigurationFolderPath(IBuildDetail detail)
91: {
92: try
93: {
94: return detail.ConfigurationFolderPath;
95: }
96: catch (Exception)
97: {
98: Debug.WriteLine(detail.ConfigurationFolderUri);
99: return "/<deleted>/<deleted>/<deleted>";
100: }//try
101: }
The Team Foundation Build Managed Reference API is well documented and not hard to use. To query the builds I used the IBuildServer.QueryBuilds method: with the IBuildDetailSpec you limit the result set returned. Keep in mind that in the database are recorded all builds including queued and running ones. I like to limit the number of records returned for better performance. This is the work lines 18 to 34. Note that there is a QueryBuilds overload that accept an array of IBuildDetailSpec in order to search for multiple build definition at a time.
After you get the data for a build, represented by a bunch of IBuildDetails, you transform and filter the results with some LINQ; obviously there are some caveats.
First of all, BuildNumber is a string, not a numerical value, so ‘1’ is greater than ‘10’, the code normalize this value in function ConvertToSortableBuildNumber (lines 67-88).
You have also to be carful in handling version control properties, like ConfigurationFolderPath: their values may be obsolete (folders has been moved, renamed or deleted) and accessing them throws an exception; this case is managed by the GetConfigurationFolderPath function (lines 90-101).
The BuildFinished property distinguish amongst completed builds (even failed) and queued or running builds: the latter may be in a transient status with a still-to-changed build number, and you may prefer to filter them out.
In the code shown, lines 50-54, I assume the TFS version control follows a certain, regular, branch structure, so I can filter the returned build data by the branch from where the code was get (see my Where the build script reside). This assumption permits to implement sophisticated rules like this: pick the highest build number of the current release branch, i.e. the one you are building, and increment the revision number — this is what I call an Hotfix build (see Builds how many?).
Now, you need to pick the reference build, upon which compute the new build identifier. This last step is a simple LINQ FirstOrDefault method that returns null in case of an empty result set: lines 55-61.
Again, such code may be packed in a MSBuild Task for TFS 2008, as in the listing above, or in a Workflow activity in TFS 2010.
Combine the two
The two ideas may be used together in two separate custom task/activity: one task to get the highest version number, the second to parse and increment it. This is a 2008 example
<Target Name="GenerateBuildVersion">
<ItemGroup>
<SourceBuildDefinitions Include="$(BuildDefinition)" Condition="'$(IsDeveloperBuild)'=='true'" />
<SourceBuildDefinitions Include="Daily" Condition="'$(IsDeveloperBuild)'!='true'" />
<SourceBuildDefinitions Include="Release" Condition="'$(IsDeveloperBuild)'!='true'" />
<SourceBuildDefinitions Include="Daily.Hotfix" Condition="'$(IsDeveloperBuild)'!='true' and '$(IsHotfixBuild)'=='true'" />
<SourceBuildDefinitions Include="Release.Hotfix" Condition="'$(IsDeveloperBuild)'!='true' and '$(IsHotfixBuild)'=='true'" />
</ItemGroup>
<ItemGroup>
<!-- get highest number only for the current branch -->
<BranchFilter Include="$(BranchFraction)" Condition="'$(IsHotfixBuild)'=='true'" />
<BranchFilter Include="$(BranchFraction)" Condition="'$(IsDeveloperBuild)'=='true'" />
</ItemGroup>
<GetHighestBuildNumber Condition="'$(HighestBuildNumber)'==''"
TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
TeamProject="$(TeamProject)"
SourceBuildDefinitions="@(SourceBuildDefinitions)"
Branches="@(BranchFilter)"
HistoryDepth="5">
<Output TaskParameter="BuildNumber" PropertyName="HighestBuildNumber" />
</GetHighestBuildNumber>
<!-- *.Hotfix -->
<IncrementBuildNumber Condition="'$(IsHotfixBuild)'=='true'"
BuildDefinitionName="$(BuildDefinition)" UseCurrentName="false" LastBuildNumber="$(HighestBuildNumber)"
IncrementBuild ="false"
IncrementRevision ="true" >
<Output TaskParameter="Major" PropertyName="Major" />
<Output TaskParameter="Minor" PropertyName="Minor" />
<Output TaskParameter="Build" PropertyName="Build" />
<Output TaskParameter="Revision" PropertyName="Revision" />
<Output TaskParameter="BuildNumber" PropertyName="BuildNumber" />
</IncrementBuildNumber>
<!-- Daily / Release -->
<IncrementBuildNumber Condition="'$(IsDeveloperBuild)'!='true' and '$(IsHotfixBuild)'!='true'"
BuildDefinitionName="$(BuildDefinition)" UseCurrentName="false" LastBuildNumber="$(HighestBuildNumber)"
IncrementBuild ="true"
IncrementRevision ="false" >
<Output TaskParameter="Major" PropertyName="Major" />
<Output TaskParameter="Minor" PropertyName="Minor" />
<Output TaskParameter="Build" PropertyName="Build" />
<Output TaskParameter="Revision" PropertyName="Revision" />
<Output TaskParameter="BuildNumber" PropertyName="BuildNumber" />
</IncrementBuildNumber>
</Target>
GetHighestBuildNumber compute the HighestBuildNumber property which is incremented by the IncrementBuildNumber task.
Note: passing HighestBuildNumber you force the build number, which is useful to reconstruct an old build or something went amok in your installation.
A bunch of properties drive the algorithm that eventually increments the version number; in this example the build number is unique between daily and release builds, hotfixes simply increments the revision number relative to the branch. This latter allows to have a 1.0 and a 1.1 release branches and the revision number is relative to the release; see also the “Builds how many?” post.
Whew! This is really a long post, with lot of code, but I hope someone may find it useful. The full source is in BuildIdentifierTasks.zip.
Happy Build!