Adding Build Steps to Team Build in orcas

Way back in August I did a post (my 2nd ever!) on adding build steps to Team Build using a custom task.  I thought I would revisit this topic in the context of the latest version of Team Build, now available in the March CTP of Orcas.

In Orcas, you'll have two options for adding build steps - the built-in BuildStep task, or your own custom task using the brand new Team Build Object Model (OM). 

The BuildStep task can be used in two distinct ways: you can add build steps and you can update build steps: 

   <Target Name="DoStuff">

    <BuildStep TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
               BuildUri="$(BuildUri)"
               Message="Doing some stuff...">
      <Output TaskParameter="Id" PropertyName="StepId" />
    </BuildStep>

    <!-- Do some stuff here... -->

    <BuildStep TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
               BuildUri="$(BuildUri)"
               Id="$(StepId)"
               Status="Succeeded" />

    <OnError ExecuteTargets="MarkBuildStepAsFailed" />
    
  </Target>

  <Target Name="MarkBuildStepAsFailed">
    
    <BuildStep TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
               BuildUri="$(BuildUri)"
               Id="$(StepId)"
               Status="Failed" />
    
  </Target>

In this example, a build step is added, some stuff happens, and the build step is updated.  Note the error handling which marks the step as Failed when an error occurs in the DoStuff target.

If you don't have any stuff to do, and just want to add an informational build step, you can ignore the ID output property and set the Status immediately when you add the build step.

     <BuildStep TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
               BuildUri="$(BuildUri)"
               Message="This is an informational message."
               Status="Succeeded" />

And that's it! 

There are still situations where you'll want to add build steps using your own custom tasks, of course.  In this case, you can use the new OM, which should be a bit simpler than using the old web service methods. 

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

namespace CustomTasks
{
    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 BuildServer service of the TF Server.
        /// </summary>
        protected IBuildServer BuildServer
        {
            get
            {
                if (m_buildServer == null)
                {
                    m_buildServer = (IBuildServer)Tfs.GetService(typeof(IBuildServer));
                }
                return m_buildServer;
            }
        }

        /// <summary>
        /// Lazy init property that gives access to the Build specified by BuildUri.
        /// </summary>
        protected IBuildDetail Build
        {
            get
            {
                if (m_build == null)
                {
                    m_build = (IBuildDetail)BuildServer.GetBuild(new Uri(BuildUri), null, QueryOptions.None);
                }
                return m_build;
            }
        }

        /// <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()
        {
            m_buildStep = InformationNodeConverters.AddBuildStep(Build, GetBuildStepName(), GetBuildStepMessage());
        }

        private void UpdateBuildStep(bool result)
        {
            m_buildStep.Status = result ? BuildStepStatus.Succeeded : BuildStepStatus.Failed;
            m_buildStep.FinishTime = DateTime.Now;
            m_buildStep.Save();
        }

        private void AddExceptionBuildStep(Exception e)
        {
            try
            {
                IBuildStep buildStep = InformationNodeConverters.AddBuildStep(Build, "Exception", e.Message, 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 IBuildServer m_buildServer;
        private IBuildDetail m_build;
        private IBuildStep m_buildStep;
    }
}

There are a few things here which require some explanation. 

  • IBuildServer.

IBuildServer is the top-level interface in the OM.  It exposes methods for getting/querying builds, build definitions (formerly known as build types), build agents (formerly non-existent), etc. 

  • IBuildDetail.

IBuildDetail represents an individual build in the OM.  It exposes methods such as Stop and Delete; properties such as StartTime and RequestedBy; and events (which support polling for changes) such as StatusChanging and PollingCompleted.  It also exposes a tree of arbitrary build information nodes which are used to store all standard Team Build data and can also be used to add any arbitrary custom data.

  • InformationNodeConverters and IBuildStep.

The InformationNodeConverters class can be used to get (and add) standard Team Build data types from the generic build information nodes associated with a build.  Each well-known data type is exposed as an interface - build steps are exposed via IBuildStep. 

I'll go into more details on the OM in later posts - hopefully this will be enough info to get those of you playing around with the March CTP started and to get those of you not playing around with the CTP interested in doing so!