TFS Build: How To Customize Work Item Association

Recently I got involved in implementing Team Foundation Server 2012 for a large development project. And even though I’ve worked with several versions of TFS over the years, this was the first time I really dove into the options for customization beyond your average workflow modifications. I’ve picked up some interesting tips and tricks over the last couple of months. Most of these customizations were already available somewhere on the web (see for example Ewald Hofman’s excellent series on customizing Team Build 2010, which in general applies quite nicely to TFS 2012 as well), but some modifications are my original work in that I did not find it anywhere else on the web. I thought I’d share some of the things I encountered.

A standard build based on the default build template will associate changesets and work items to builds. The way it does that is by retrieving the label for the previous successful build for the given build’s Build Definition, and by determining which changesets are included in the current build that were not included in the previous build. Some or all of those changesets might have work items associated with them, and those work items get associated with the build. Assuming the default build template, this is done as part of the “Compile, Test, and Associate Changesets and Work Items” parallel sequence. Look for a sequence like this in the template editor:

AssociateChangesetsAndWorkitems

Now if your team is anything like my team, they will not deliver a specific piece of functionality or fix a particular bug in a single checkin. A single functional requirement as documented in a Product Backlog Item may require the involvement of multiple persons, all with different specialties. So if one team member checks in his changes and associates it with a Task, that does not mean that the Task’s parent Product Backlog Item should be considered part of the next build. And the opposite can also be true. Let’s assume, for example, a scenario in which the project’s goal is to deliver a standard software product (an ERP system for example) and some customizations to go with it. Let’s also assume that the metadata for this ERP system does not reside in TFS. Instead, during a release build, the metadata is extracted from the development environment, committed to TFS, and from there pushed to QA, and ultimately to Production. Now, if a bug is filed, the bug may very well be caused by some configuration setting in the ERP system. The ERP guy on the team fixes the bug by modifying the configuration, which is pushed to QA with the next release. The bug is then marked as Resolved, but TFS Build will never associate this bug with a build because no changeset occurred that contained the fix.

To solve this problem, I tried my hand at an alternative method for associating a build with work items. What I wanted was to associate all work items that have a specific status (such as Done or Resolved), and are not yet associated to any previous builds. That means that we have to query TFS to get that specific set of work items, since the set is no longer a function of the changesets for this build. Now, querying TFS for work items is pretty well covered on the web, so I’m not going to elaborate on how to do that. The relevant part here is that the result of this query is stored in a variable scoped to the topmost Sequence, and that the query is the first thing that gets executed on the Build Agent. This is to ensure that the correct work items are retrieved before the sources are downloaded so as to avoid synchronicity issues. This variable is then used as a parameter in a custom build activity. For each work item in the list, the activity sets the value of the IntegrationBuild field to the build number, and it associates the work item with the build. The code for this activity looks something like the following:

using System;
using System.Activities;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Microsoft.TeamFoundation.Build.Client;
using Microsoft.TeamFoundation.Build.Workflow.Activities;
using Microsoft.TeamFoundation.VersionControl.Client;
using Microsoft.TeamFoundation.WorkItemTracking.Client;

namespace CustomActivities
{
    public sealed class AssociateResolvedWorkItems: CodeActivity
    {
        [RequiredArgument]
        public InArgument<IList> WorkItems { get; set; }

        protected override void Execute(CodeActivityContext context)
        {
            var workItems = context.GetValue(WorkItems);

            var buildDetail = context.GetExtension<IBuildDetail>();
            var store = buildDetail.BuildServer.TeamProjectCollection.GetService<WorkItemStore>();

            foreach (WorkItem workItem in workItems)
            {
                // Update the workitem
                workItem.SyncToLatest();
                workItem.PartialOpen();
                workItem["Microsoft.VSTS.Build.IntegrationBuild"] = buildDetail.BuildNumber;
                workItem.History = ActivitiesResources.Format(ActivitiesResources.BuildUpdateForWorkItem, null);
            }
            var array = workItems.ToArray();
            var errors = store.BatchSave(array);

            var associated = new List();

            foreach (WorkItem workItem in workItems)
            {
                var error = errors.FirstOrDefault(item => item.WorkItem.Id == workItem.Id);
                if (error != null)
                {
                    // Alert update error
                    context.TrackBuildWarning(String.Concat("Unable to associate work item '", workItem.Id.ToString(CultureInfo.InvariantCulture), "': '", workItem.Title,
                                      "' - ", error.Exception), BuildMessageImportance.High);
                }
                else
                {
                    // Write update message...
                    context.TrackBuildMessage(String.Concat("The work item '", workItem.Id.ToString(CultureInfo.InvariantCulture), "' was updated with build label '",
                                      buildDetail.BuildNumber, "'."), BuildMessageImportance.Low);
                    // ... and add to the list of work items to associate
                    associated.Add(workItem);
                }
            }

            // Associate updated workitems to build
            buildDetail.Information.AddAssociatedWorkItems(associated.ToArray());
        }
    }
}

You see that this is a two-way association. First, the build number is set as the value of the IntegrationBuild field on the work item. This makes sure that work item queries that use this field still work as expected. Second, the work item is added to the Build Information. This makes sure that the work items are displayed on the Build Details. And as an added bonus, Microsoft Test Manager’s Recommended Tests view also correctly displays the list of work items when comparing two builds.

The activity is inserted in the template at the same place as where the standard associations occurred:

AssociateChangesetsAndWorkitemsCustomized

One final thing to do is determine whether you also still want the standard association (based on changeset-related work items) to occur. If you don’t want that, the “Associate Changesets And Workitems” activity (which appears right above our newly inserted activity) has an interesting parameter for you to edit. If you right-click on the activity and select Properties, you notice that one of the parameters is named “UpdateWorkItems” and has the value ‘True’. Simply changing that to ‘False’ will make sure that only the work items you selected are associated with the build.

Leave a comment