Sunday, June 24, 2012

Gathering TFS Build Information for Continuous Integration

I've moved to a new .Net based project where we use TFS as our build system. The project is pretty large with 150 developers and over 65 deployable applications, programs and NuGet packages.  The CI, systems integration and stable branch builds results in over 200 different builds on the TFS server.  I'm mostly interested in the Continuous Integration builds, 65 build,s that I'd like to monitor. I really don't want to manage 65 lights, even though that's an excuse for a new hardware project. In this case I'll treat the 65 builds as if they are a single composite build tied to a single status light.  I'll do the same with each of the branch / target types so that I have 3 sets of 65 builds to monitor.

The first step is to write some C# code that talks to TFS to get the status of the builds I'm interested in. Microsoft provides a .Net compatible library for communicating with TFS in the Microsoft.TeamFoundation set of libraries. This provides a simple interface that hides the web API. 

Our TFS server is behind a firewall tied to an Active Directory domain other than our internal domain so I must explicitly specify my credentials in the configuration file. 

We connect to the TFS server through the TfsTeamProjectCollection class which accepts the connection URL an a set of credentials.  We then use that TFS connection to get access to the BuildServer (IBuildServer) itself. A build server can have multiple teams on it each with it's own TeamProject . Each Team Project can have multiple build definitions, each with its own set of builds. In my case all my builds are on a single project in a single collection.

To recap, we retrieve our TFS information like this:
  • Connect TFS Team Project Collection using a URL 
    • Select the TFS Team Project by name
      • Create a query that finds the last two builds for all Build Definitions that m match our build definition pattern. Earlier versions first found all the Build Definitions and then queried TFS for the build results for each definition separately.  This resulted in a lot more queries but not much slower response time.
      • Send the results to the attached build light
    • Repeat for each set of builds / lamp 


Source is available on GitHub at Firmware sources for feedback devices is also available on github


I created a couple reusable classes to manage the data for this task.

BuildWatchDriver.cs The main driver class that reads App.config properties, configures communications and acts a run loop.

IBuildIndicatorDevice.cs A simple interface implemented by each of the physical device adapters. This is the interface to the build lights.

TfsBuildAdapter.cs This acts as a proxy for TFS including all of the utility methods and support for communicating with the TFS build server. There is one per set of builds that are monitored as a single set

TfsBuildConnection.cs This class is used to manage the network connection to TFS providing a place to store connection strings and the build server instances resulting from the authentication to the server.  One instance can be shared across TfsBuildAdapter instances as long as they run single threaded.

LastTwoBuildResults.cs A "bag" class that holds a build definition and the last two build results.  This provides a simple way of passing around and handling related data. One instance is created for each build definition in the build set.

ArduinoDualRGB.cs Driver class for an Arduino based build lamp. The firmware for the lamp is also available on GitHub.

CheapLaunchpadMSP430.cs Driver class a single lamp $10 build-light based on the TI Launchpad development board a common anode RGB LED, 3 50 ohm resistors and a gift card tin case.

Freemometer.cs Driver class for an analog gauge and LED output device.  Another blog article discusses it's construction out of an Arduino , Ikea Dekad clock and a hobby servo.

SimulatedDEvice.cs Dummy driver class that simulates a hardware device. This can be used by folks wanting to test the application who dont have any actual build light hardware.


Connection information is stored in App.Config.  The TFS URL includes the collection name so it is more than just http://domain/tfs.  We specify full credentials to allow cross domain authentication.

    <add key="Tfs.Url" value="" />
    <add key="Tfs.Username" value="userid" />
    <add key="Tfs.Password" value="password" />
    <add key="Tfs.Domain" value="your_ad_domain" /> 

Connection information is stored in App.Config.  The TFS URL includes the collection name so it is more than just http://domain/tfs.  We specify full credentials to allow cross domain authentication.

Build definitions are retrieved by pattern using a query-spec.  We've spring wired the specification into our build adapters using Spring.Net.

  <!-- There is one build adapter for each light, each group of builds monitored as a group-->
  <!-- this is set up with lazy-init=true because we let the program first verify the server connection before bringing everyithing up -->
  <object id="myBuildAdapters" type="System.Collections.Generic.List&lt;BuildWatcher.Tfs.TfsBuildAdapter>" lazy-init="true" >
    <constructor-arg name="collection">
    <list element-type="BuildWatcher.Tfs.TfsBuildAdapter, BuildWatcher">
      You should only enable as many build adapters as you have lights!
      constructor-args are
      TFS Build Server connection
      TFS Team Project name
      TFS Build definition pattern
    <!-- specified constructor argument names to make it more obvious what is going on. They are not required -->
    <object type="BuildWatcher.Tfs.TfsBuildAdapter, BuildWatcher">
      <constructor-arg name="connection" ref="myBuildServerConnection" />
      <constructor-arg name="teamProjectName" value="MSI" />
      <constructor-arg name="definitionNamePattern" value="CI_vNext*" />
    <object type="BuildWatcher.Tfs.TfsBuildAdapter, BuildWatcher">
      <constructor-arg name="connection" ref="myBuildServerConnection" />
      <constructor-arg name="teamProjectName" value="MSI" />
      <constructor-arg name="definitionNamePattern" value="V_vNext*" />
    <object type="BuildWatcher.Tfs.TfsBuildAdapter, BuildWatcher">
      <constructor-arg name="connection" ref="myBuildServerConnection" />
      <constructor-arg name="teamProjectName" value="MSI" />
      <constructor-arg name="definitionNamePattern" value="P_Main*" />
    <object type="BuildWatcher.Tfs.TfsBuildAdapter, BuildWatcher">
      <constructor-arg name="connection" ref="myBuildServerConnection" />
      <constructor-arg name="teamProjectName" value="MSI" />
      <constructor-arg name="definitionNamePattern" value="V_RC*" />

The actual query is in TfsBuildAdapter.cs using the IBuildDetailSpec.  

  IBuildDetailSpec buildDetailsQuerySpec;
  if (buildDefinitionPattern != null)
      buildDetailsQuerySpec = this.Connection.BuildServer.CreateBuildDetailSpec(teamProject.Name, buildDefinitionPattern);
    buildDetailsQuerySpec = this.Connection.BuildServer.CreateBuildDetailSpec(teamProject.Name);
  //// Failure to set this property results in ALL of the build information
  //// being retrieved resulting in 10X+ call times
  //// You can retrieve subsets with something like
  //// buildDetailsQuerySpec.InformationTypes = new string[] { "ActivityTracking", "AgentScopeActivityTracking" };
  buildDetailsQuerySpec.InformationTypes = null;
  //// last and previous
  buildDetailsQuerySpec.MaxBuildsPerDefinition = 2;
  //// use start time descending because InProgress builds don't seem to sort correctly when using EndTimeDescending
  buildDetailsQuerySpec.QueryOrder = BuildQueryOrder.StartTimeDescending;
  IBuildQueryResult buildResults = this.Connection.BuildServer.QueryBuilds(buildDetailsQuerySpec);

Additional Features

The build monitor also supports a http server so that you can check the status of the builds using a web browser. This is also enabled via the App.config.  You can pick any port or trailing url you want.

<!-- ***********************************************************************
      This key causes starts a small build status web server on this port
      "+" means all hosts on all paths
      "*" means all hosts on a psecified path
      trailing "/" is required
      YOU MUST ENABLE self hosting if with this command as Administrator
      netsh http add urlacl url=http://+:8080/ user=machine\username
      You shoul disable self hosting if you don't run this app any more
      netsh http delete urlacl url=http://+:8080/
      Failure to do this will result in no web services but the app will still run
      *********************************************************************** -->
 <add key="HttpListener.ServiceUri" value="http://+:8080/" />


Last Edited 11/4/2012

No comments:

Post a Comment