Associating changesets and Work Items Since the Last Successful Build

We got a forum post the other day on whether changesets and work items could be associated since the last Successful build.  Some of you may be thinking "Isn't that how it already works?"  Actually, it's not quite how it works.  Changesets and Work Items are, by default, associated since the last "good" build, where "good" means compilation and tests succeeded, but something else may have gone wrong.  That is, the last "good" build may well be marked Partially Succeeded and not Succeeded.

I started responding to the forum post, and then figured it would make a good blog post instead.  (Of course, I've posted a link to this post in the forums...)

So - to modify the default behavior and associate changesets and work items only since the last successful build, you'll need to do three things:

  1. Skip the default logic to associate changesets and work items.  This is pretty simple - just set the SkipGetChangesetsAndUpdateWorkItems property to true.
  2. Write a custom task to find the last successful build and return its label.
  3. Call this custom task and then call the GenCheckinNotesUpdateWorkItems task with its output.

Lucky for you, I've already written the custom task (or at least a simple version of it).  Here is the source for it:

 using System;
using Microsoft.Build.Framework;
using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.Build.Client;

namespace BlogProjects
{
    public class GetLastSuccessfulBuildLabel : ITask
    {
        #region Properties

        [Required]
        public String BuildDefinitionName
        {
            get
            {
                return m_buildDefinitionName;
            }
            set
            {
                m_buildDefinitionName = value;
            }
        }

        [Output]
        public String LastSuccessfulBuildLabel
        {
            get
            {
                return m_lastSuccessfulBuildLabel;
            }
        }

        [Required]
        public String TeamFoundationServerUrl
        {
            get
            {
                return m_teamFoundationServerUrl;
            }
            set
            {
                m_teamFoundationServerUrl = value;
            }
        }

        [Required]
        public String TeamProject
        {
            get
            {
                return m_teamProject;
            }
            set
            {
                m_teamProject = value;
            }
        }

        #endregion

        #region ITask Members

        public bool Execute()
        {
            TeamFoundationServer tfs = TeamFoundationServerFactory.GetServer(TeamFoundationServerUrl);
            IBuildServer buildServer = (IBuildServer)tfs.GetService(typeof(IBuildServer));

            IBuildDetailSpec spec = buildServer.CreateBuildDetailSpec(TeamProject, BuildDefinitionName);
            spec.MaxBuildsPerDefinition = 1;
            spec.Status = BuildStatus.Succeeded;
            spec.QueryOrder = BuildQueryOrder.FinishTimeDescending;

            IBuildQueryResult queryResult = buildServer.QueryBuilds(spec);

            if (queryResult.Builds.Length > 0)
            {
                m_lastSuccessfulBuildLabel = queryResult.Builds[0].LabelName;
            }

            return true;
        }

        public IBuildEngine BuildEngine
        {
            get
            {
                return m_buildEngine;
            }
            set
            {
                m_buildEngine = value;
            }
        }

        public ITaskHost HostObject
        {
            get
            {
                return m_taskHost;
            }
            set
            {
                m_taskHost = value;
            }
        }

        #endregion

        #region Private Members

        private IBuildEngine m_buildEngine;
        private ITaskHost m_taskHost;
        private String m_teamFoundationServerUrl;
        private String m_buildDefinitionName;
        private String m_teamProject;
        private String m_lastSuccessfulBuildLabel;

        #endregion
    }
}

Note the fancy querying that is possible with an IBuildDetailSpec - pretty cool... 

Calling the custom task should be similarly simple.  Something like the following should do the trick:

   <Target Name="BeforeGetChangesetsAndUpdateWorkItems">

    <GetLastSuccessfulBuildLabel TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
                                 TeamProject="$(TeamProject)" 
                                 BuildDefinitionName="$(BuildDefinitionName)">
      <Output TaskParameter="LastSuccessfulBuildLabel" PropertyName="LastSuccessfulBuildLabel" />
    </GetLastSuccessfulBuildLabel>

    <GenCheckinNotesUpdateWorkItems TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
                                    BuildUri="$(BuildUri)"
                                    BuildNumber="$(BuildNumber)"
                                    CurrentLabel="$(LabelName)@$(LabelScope)"
                                    LastLabel="$(LastSuccessfulBuildLabel)"
                                    UpdateWorkItems="$(UpdateAssociatedWorkItems)"
                                    ContinueOnError="true" />

  </Target>

Hope this helps!

UPDATE: My original post had a typo - the property SkipGetChangesetsAndUpdateWorkItems was missing its And.  My apologies to anyone who banged their head against the wall trying to figure out why it wasn't working!