Oct 042011

The default TFS build template has a great feature where you can include in the build summary a list of changesets and work items that relate to the build. This is very useful, but under certain circumstances the list gets longer and longer – the build process correctly associates new changes with the build, but does it cumulatively – associating the same changes and work items with many successive builds. This erodes the usefulness of the section in the build summary, and creates a long history of work item updates saying “The Fixed In field was updated as part of associating work items with the build” making the history a mess and confusing your users.

To fix this we first have to understand how TFS is finding which changesets and work items to associate with the build. This is how it works:

  1. The build labels the source code. You can see this in the detailed build log under the message “Create Label”, followed by “Label {BUILDNAME}@$/{TEAMPROJECT} ….. was successfully created.”
  2. The build calls a Team Foundation Build Activity called AssociateChangesetsAndWorkItems. You can see where this happens in the build log at the message “Associate Changesets and Work Items”.
  3. The AssociateChangesetsAndWorkItems activity seems like a black box but it writes a key piece of information to the build log: “Analyzing labels {PREVIOUSBUILDNAME} and {CURRENTBUILDNAME}”. This shows that it is comparing source labels to figure out which changesets have been checked in between the two builds.
  4. When the changesets have been identified it’s easy for TFS to find the work items, using changeset links. When you choose a work item on check-in a changeset link is created linking changeset and work item together. You can see this link from either side – in the work items channel of the changeset details dialog (accessible from the source history) and on the All Links tab of work items.

If you see your list of associated changesets and work items getting longer and longer then the build log is the first place to look. The “Analyzing labels…” entry will make it clear which build it is comparing to. If that “baseline” build is way back – and not changing between successive builds – that’s why the list is forever growing.

So how does TFS decide which build to choose as the baseline? The default behaviour is to use the last successful build of the same definition – meaning fully successful builds only, not partially successful builds (i.e. with one or more test failures). So you may have many consecutive partially successful builds but TFS will ignore them for the purpose of associating changesets and work items.

Now we understand the cause we can design a fix. Handily, the AssociateChangesetsAndWorkItems activity comes with some useful input arguments: CurrentLabel, LastLabel, and UpdateWorkItems. In the default template, both the label arguments are blank so the activity will work out which to use, but we can provide our own values to override that behaviour. If we determine the label for the last build which was successful or partially successful we will fix the ever-growing changesets and work items problem. (We are putting aside here the concern we should have over recurrent test failures – making this change shouldn’t be an excuse for not fixing test failures).

First (assuming you have set up a solution for creating custom build activities) you need to create an activity which gets the label we will pass in via the LastLabel argument. Here’s an example:

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

namespace BuildTasks.Activities
/// The default AssociateChangesetsAndWorkItems activity compares the current build with
/// the last completely successful build. If builds have been consistently partially successful rather
/// than fully successful the list of changesets and work items will accumulate items ad infinitum.
/// This activity returns the label for the last fully OR partially successful build, which can then
/// be used when calling the AssociateChangesetsAndWorkItems activity, so only changesets and work
/// items since that build are associated.
/// </summary>

public sealed class GetLastSuccessfulBuildLabel : CodeActivity<string>
/// </summary>

/// <param name="context"></param>
/// <returns>Label applied for last successful (full or partial) build of this definition</returns>

protected override string Execute(CodeActivityContext context)
string label = "";

IBuildDetail buildDetail = context.GetExtension<IBuildDetail>();
IBuildServer buildServer = buildDetail.BuildServer;
IBuildDefinition buildDefinition = buildDetail.BuildDefinition;

IBuildDetailSpec spec = buildServer.CreateBuildDetailSpec(buildDefinition);
spec.MaxBuildsPerDefinition = 1;
spec.Status = BuildStatus.Succeeded | BuildStatus.PartiallySucceeded;
spec.QueryOrder = BuildQueryOrder.FinishTimeDescending;

IBuildQueryResult queryResult = buildServer.QueryBuilds(spec);

if (queryResult.Builds.Length > 0)
// label can be null if build was run without "Label Sources" option
label = queryResult.Builds[0].LabelName ?? "";

// labels are returned in the format {LABELNAME}@$/{TEAMPROJECT},
// but we just want the label before the @ sign
if (label.Contains("@"))
label = label.Substring(0, label.IndexOf("@"));

return label;

The activity needs to be added to your library of custom build activities, building and deploying. Next, you need to modify the build template (best to use a copy of the default template) to make use of the new activity and pass its result into the LastLabel argument of AssociateChangesetsAndWorkItems. The most logical place to put this is just before the AssociateChangesetsAndWorkItems activity itself:


<If Condition="[AssociateChangesetsAndWorkItems]" DisplayName="If AssociateChangesetsAndWorkItems" sap:VirtualizedContainerService.HintSize="464,201" mtbwt:BuildTrackingParticipant.Importance="Low">
<mtbwa:InvokeForReason DisplayName="Associate Changesets and Work Items for non-Shelveset Builds" sap:VirtualizedContainerService.HintSize="222,208" Reason="Manual, IndividualCI, BatchedCI, Schedule, ScheduleForced, UserCreated">
<Variable x:TypeArguments="x:String" Name="LastSuccessfulBuildLabel" />
<ba:GetLastSuccessfulBuildLabel DisplayName="Get Last Successful Build Label" sap:VirtualizedContainerService.HintSize="200,22" mtbwt:BuildTrackingParticipant.Importance="Low" Result="[LastSuccessfulBuildLabel]" />
<mtbwa:AssociateChangesetsAndWorkItems DisplayName="Associate Changesets and Work Items" sap:VirtualizedContainerService.HintSize="200,22" LastLabel="[LastSuccessfulBuildLabel]" Result="[associatedChangesets]" UpdateWorkItems="[UpdateWorkItems]" />


Note that the and elements are unchanged from the default template – I have included them to indicate where the change has been made. I create a variable called LastSuccessfulBuildLabel to hold the result and pass that into the LastLabel argument. Note that I also parameterized the UpdateWorkItems argument, but you don’t have to do that to fix this problem (I find it useful to associate changesets but leave work items alone when I have multiple build definitions running against the same branch of code – I don’t want to update work items more than necessary). Of course, you may well find it easiest to make this change using the workflow designer. Here’s a snippet of what it should look like:

So there’s the fix! I hope someone finds that useful.

Note: labelling the source code and associating changesets and work items can be toggled off in the build definition – both need to be turned on for the feature to work.

  6 Responses to “Ever Expanding Associated Changesets and Work Items”

  1. Brilliant – very useful post, thanks for putting it up here…

  2. Had the same problem, your solution worked a treat. Many thanks, Peter.

  3. ” I create a variable called LastSuccessfulBuildLabel to hold the result and pass that into the LastLabel argument.” – How was it done

    I tried adding a variable. not working!

  4. Thank you for your post. AssociateChangesetsAndWorkItems has been working well, but we needed to customize it by inserting specific starting and ending build labels. So I coded up something to pass in the specific starting and ending build label for which I wanted to calculate changesets/work items.

    I was confused when it didn’t work until I realized that what they call “CurrentLabel” is the ending label and what they call “LastLabel” is the starting label.

    This may be obvious to others, but I found it a bit confusing so thought I would post this comment. Maybe it will help someone.

  5. Thanks a ton for this post. There isn’t a lot of documentation on the inner workings of these TFS tasks and your site was just about the only one on the net that I could find with any relevant information.

    Your code helped me troubleshoot an issue i was having with a custom workflow after upgrading from TFS2012 to TFS2013.

    I discovered via Reflector that if the LastLabel argument is empty, the task will use BuildDetail.BuildDefinition.LastGoodBuildLabel. Similarly if CurrentLabel is empty it will use BuildDetail.LabelName.

    If you find that LastGoodBuildLabel is returning null, it’s because there have been no prior builds that TFS has deemed successful. After much trial and error, I’ve determined that TFS will only set the LastGoodBuildLabel to the LabelName of the current build if and only if all of these are true:

    BuildDetail.Status = BuildStatus.Succeeded
    BuildDetail.CompilationStatus = BuildPhaseStatus.Succeeded
    BuildDetail.TestStatus = BuildPhaseStatus.Succeeded

    Thanks again for the helpful post!

  6. Very USEFUL!!!

    I had to do a small change: Return the ‘IBuildDetail’ instead, to use it with the AssociateChanges activity.
    * Also changed the variable type, base type, and class name(To reflect change).

    But this solve my problems

    Thank you

Sorry, the comment form is closed at this time.