Adding BuildSteps to Team Build through a Custom Task

Team Build displays Build Steps in the build report form within Visual Studio.  By default, build steps are added at various points during the course of a build - while getting sources (in the Get task), compiling solutions / projects, copying files to the drop location, etc.  Team Build allows users to insert their own build steps using the publicly accessible BuildStore web service - in particular, the AddBuildStep and UpdateBuildStep methods.  The following sample (I make no claims as to the awesomeness or lack thereof of this sample, etc.) TeamBuildTask class illustrates how this can be done:

using System;
using System.Web.Services;
using Microsoft.Build.Framework;
using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.Build.Common;
using Microsoft.TeamFoundation.Build.Proxy;

/// <summary>
/// Summary description for Class1
/// </summary>
namespace MyNamespace
{
    public abstract class TeamBuildTask : ITask
    {
        /// <summary>
        /// Put real task logic in this method.
        /// </summary>
        /// <returns>True if task is successful, otherwise false.</returns>
        protected abstract bool ExecuteInternal();

        /// <summary>
        /// Returns the name of the build step to be added for this task.
        /// </summary>
        /// <returns>Name of the build step to be added.</returns>
        protected abstract string GetBuildStepName();

        /// <summary>
        /// Returns the message of the build step to be added for this task - this is the
        /// string displayed in the Team Build GUI.
        /// </summary>
        /// <returns>Message of the build step to be added.</returns>
        protected abstract string GetBuildStepMessage();

        /// <summary>
        /// ITask implementation - BuildEngine property.
        /// </summary>
        public IBuildEngine BuildEngine
        {
            get
            {
                return m_buildEngine;
            }
            set
            {
                m_buildEngine = value;
            }
        }

        /// <summary>
        /// ITask implementation - HostObject property.
        /// </summary>
        public ITaskHost HostObject
        {
            get
            {
                return m_hostObject;
            }
            set
            {
                m_hostObject = value;
            }
        }

        /// <summary>
        /// The Url of the Team Foundation Server.
        /// </summary>
        [Required]
        public string TeamFoundationServerUrl
        {
            get
            {
                return m_tfsUrl;
            }
            set
            {
                m_tfsUrl = value;
            }
        }

        /// <summary>
        /// The Uri of the Build for which this task is executing.
        /// </summary>
        [Required]
        public string BuildUri
        {
            get
            {
                return m_buildUri;
            }
            set
            {
                m_buildUri = value;
            }
        }

        /// <summary>
        /// Lazy init property that gives access to the TF Server specified by TeamFoundationServerUrl.
        /// </summary>
        protected TeamFoundationServer Tfs
        {
            get
            {
                if (m_tfs == null)
                {
                    if (String.IsNullOrEmpty(TeamFoundationServerUrl))
                    {
                        // Throw some exception.
                    }
                    m_tfs = TeamFoundationServerFactory.GetServer(TeamFoundationServerUrl);
                }
                return m_tfs;
            }
        }

        /// <summary>
        /// Lazy init property that gives access to the BuildStore service of the TF Server.
        /// </summary>
        protected BuildStore BuildStore
        {
            get
            {
                if (m_buildStore == null)
                {
                    m_buildStore = (BuildStore)Tfs.GetService(typeof(BuildStore));
                }
                return m_buildStore;
            }
        }

        /// <summary>
        /// ITask implementation - Execute method.
        /// </summary>
        /// <returns>
        /// True if the task succeeded, false otherwise.
        /// </returns>
        public bool Execute()
        {
            bool returnValue = false;

            try
            {
                AddBuildStep();
                returnValue = ExecuteInternal();
            }
            catch (Exception e)
            {
                AddExceptionBuildStep(e);
                throw;
            }
            finally
            {
                UpdateBuildStep(returnValue);
            }

            return returnValue;
        }

        private void AddBuildStep()
        {
            BuildStore.AddBuildStep(BuildUri, GetBuildStepName(), GetBuildStepMessage());
        }

 

        private void UpdateBuildStep(bool result)
        {

            BuildStepStatus status = result ? BuildStepStatus.Succeeded : BuildStepStatus.Failed;
            BuildStore.UpdateBuildStep(BuildUri, GetBuildStepName(), DateTime.Now, status);
        }

        private void AddExceptionBuildStep(Exception e)
        {
            try
            {
                BuildStore.AddBuildStep(BuildUri, "Exception", e.Message);
                BuildStore.UpdateBuildStep(BuildUri, "Exception", DateTime.Now, BuildStepStatus.Failed);
            }
            catch
            {
                // Eat any exceptions.
            }
        }

        private IBuildEngine m_buildEngine;
        private ITaskHost m_hostObject;
        private string m_tfsUrl;
        private string m_buildUri;
        private TeamFoundationServer m_tfs;
        private BuildStore m_buildStore;
    }
}

To use this base class, just override the ExecuteInternal method - put the actual task logic here.  Then override the GetBuildStepName and GetBuildStepMessage methods to specify the Name of the build step (which serves as its ID and should therefore be reasonably unique) and the Message of the build step (which will be the string displayed in the build report form).

The Execute method will add a build step (with the specified Name and Message) when task execution starts, and update the build step with the appropriate status and time when execution completes.  If an exception is thrown by the ExecuteInternal method, an exception build step will be added (if possible) by the AddExceptionBuildStep method.

Note the TeamFoundationServerUrl and BuildUri properties.  These properties will need to be set for derived tasks to function properly, and can be easily set to the TeamFoundationServerUrl and BuildURI properties available during a Team Build (e.g. within TfsBuild.proj).

 <Target Name="BeforeBuild">
 <SomeCustomTask 
     TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
        BuildUri="$(BuildURI)" />
</Target>

Happy building!