Wednesday, August 05, 2009

How to Chain TFS Builds?

 

One of my colleagues @gdurzi recently asked me this question. Sounds straightforward enough to be supported out of the box with TFS right? Too many quirks with this. And I recommended using the ever faithful MSBuild <Exec> task to make a call to TFSBuild.exe to queue a new build from the first TFSBuild.proj with something like this

TFSBuild.exe start /queue %TFSSVR% %TEAMPROJECT% %BUILDTYPE%

An issue with using TFSBuild.exe is that you cannot pass Build agents as a command line argument which was a deal breaker for us.

There are several approaches that you can take based on your particular scenario so let’s define the scenario here, you have a Main_Build TFS build definition that builds your core project and you want the ability to have multiple staging builds running the same Main_Build for compilation/building, but be custom staged for deployment based on who calls Main_Build. Very useful when you have a product which rolls out to multiple clients with a need for custom pre-build and post-build actions per client.  So here is one way to do Build Chaining with TFS 2008.

Step 1: Let’s create a custom MSBuild task using the Team Foundation object model which queues a build using the default build agent associated with the Build definition file.

Sample code for Queuing: QueueTFS.cs

using Microsoft.TeamFoundation.Client;

using Microsoft.TeamFoundation.Build.Client;

 

// Get the team foundation server.

TeamFoundationServer _tfsServer = TeamFoundationServerFactory.GetServer(_tfs);

 

// Get the IBuildServer

IBuildServer buildServer = (IBuildServer)_tfsServer.GetService(typeof(IBuildServer));

 

// Get the build definition for which a build is to be queued.

IBuildDefinition definition = buildServer.GetBuildDefinition(teamProject, buildDefinition);

 

// Create a build request for the build definition.

IBuildRequest request = definition.CreateBuildRequest();

 

request.CommandLineArguments = "Pass any custom command line args here"; // Ex: Custom Targets file

// Queue the build.

buildServer.QueueBuild(request, QueueOptions.None);

 

Step 2: Now copy the QueueTFS.dll to a new folder in TFS where you want to create the staging Build definition file. Now let’s create a minimal TFSBuild.proj file which uses our new MSBuild task and overrides the EndToEndIteration target. This will be our Staging build definition which will trigger the Main_Build build. Note that you will have to create this TFSBuild.proj by hand and simply point the project file location from the Build definition UI to the new folder.

Sample code for a minimal TFSBuild.proj:

<?xml version="1.0" encoding="utf-8"?>

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="3.5">

<Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\TeamBuild\Microsoft.TeamFoundation.Build.targets" />

 

  <UsingTask TaskName="MyNewCustomTFSTask" AssemblyFile="QueueTFS.dll"/>

 

  <Target Name="EndToEndIteration">

    <Message Text="About to trigger main build" Importance="high"/>

    < MyNewCustomTFSTask TFS="http://TFSServer.com:8080/" TeamProject="TeamProject" BuildDefinition="Main_Build" TargetsFile="Custom.Target" XYZ="XYZ" />

    <!-- When everything is done, change the status of the task to "Succeeded" -->

    <SetBuildProperties TeamFoundationServerUrl="$(TeamFoundationServerUrl)" BuildUri="$(BuildUri)" TestStatus="Succeeded" CompilationStatus="Succeeded"/>

   

  </Target>

</Project>

 

Step 3: Edit your Main_Build TFSBuild.proj file with the pre-build and post-build target calls.

 

  <Target Name="BeforeCompile">

   

    <CallTarget Targets="Custom_PreBuild"/>

   

  </Target>

 

  <Target Name="AfterDropBuild" Condition="'$(BuildBreak)'!='true'">

   

    <CallTarget Targets="Custom_PostBuild"/>

   

  </Target>

 

We wanted the ability to run Main_Build by itself as well, to support this we add conditional imports in our Main_Build TFSBuild.proj to import a default targets file with empty Custom_PreBuild and Custom_PostBuild targets. $(CustomTarget) is what you would pass as a command line argument in Step 1 for request.CommandLineArguments

  <Import Project="$(CustomTarget)" Condition="'$(CustomTarget)'!=''"/>

  <!--Import CustomContoso.Target if no partner is passed in-->

  <Import Project="EmptyCustom.Target" Condition="'$(CustomTarget)'==''"/>

 

Step 4: Now create your targets file Custom.Target and EmptyCustom.Target with Custom_PreBuild and Custom_PostBuild targets and you are done.

 

I added support for updating build steps and a few other minor things which outside the scope of this blog post, but this should hopefully get you started.

 

No comments: