Howto: Compose a TFS build from the output of previous builds with a configuration file

Posted · 1 Comment

Ever needed to create a build out of a lot of smaller builds?  Let’s say that you’ve got a Team Foundation Server (TFS) build that is supposed to take a bunch of different work streams and combine them to create a single installer (MSI).  In TFS this can get a little tricky if you don’t want to recompile all the individual components.  In the case of code for medical devices, sometimes it would require millions of dollars and months of work to re-test a component if its code is recompiled.  If you recompile it and release that code without putting it through that huge and rigorous test process, you might actually be doing something illegal.  You need a way to combine these individual components without changing or touching *anything* because you have to be able to guarantee that *nothing* has changed since the last test.

Here’s an example.  You’ve got a system that will be released and it’s made up of the following pieces:

  • Patient Record Management (v1.2.4)
  • Dialysis Equipment Firmware (v2.1.1)
  • Notification System (v1.2.4)
  • Patient Scheduling (v4.0.0)

Let’s say that these pieces are all huge and are combined to create one single gigantic installer.  You’re getting ready to create a quarterly release.  The notification system has changes but nothing else has been modified.  As far as version numbers are concerned, that might look something like the following:

  • Patient Record Management (v1.2.4)
  • Dialysis Equipment Firmware (v2.1.1)
  • Notification System (v1.5.0)
  • Patient Scheduling (v4.0.0)

In order to make this work with TFS Build, you need to do some customization.  My solution was to make this all run off of a configuration script, a custom DLL that reads the config and talks to TFS, a PowerShell script, and the new TFS2013 pre-build script hook.  The assumptions are that 1) each component output is generated via a TFS build, 2) all builds are in the same Team Project Collection (TPC), and 3) there’s no guarantee that all the builds are in the same Team Project. 

(download the source code and samples for this blog post)

The Configuration File

The configuration file (composed-build-config.xml) has a configurable list of the component pieces that are going to go into the final “component” build.  Each component gets an element in the config file named <build-output /> that has attributes for 1) the friendly description of the component, 2) the TFS team project that contains it, 3) the TFS build definition name that the composed build will pull from, 4) the specific build number that will be used, 5) the optional subdirectory in the build’s drop directory that will be copied, and 6) the target location on disk that we’ll copy to as part of the composed build.  This configuration file goes into Team Foundation Server version control and is therefore change controlled and change audited. 

If you need to know what went into a given build, you can open this file and then look at the individual component build values at any point in time.  From there you can go look at the contents of the component build to see what went into that.  Since everything is in TFS, it’s all traceable.

Below is a sample composed-build-config.xml file.

<?xml version=”1.0″ encoding=”utf-8″?>
<composed-build>
  <build-output
    description=”Patient Records”
    team-project=”PatientRecs”
    build-name=”NightlyBuild”
    build-number=”NightlyBuild_20130901.1″
    drop-directory-subfolder=”_PublishedWebsites”
    to-folder=”patient-records”
    />
  <build-output
    description=”Dialysis Firmware”
    team-project=”Dialysis”
    build-name=”ReleaseCompile”
    build-number=”ReleaseCompile_20120614.26″
    drop-directory-subfolder=””
    to-folder=”firmware”
    />
  <build-output
    description=”Notifications”
    team-project=”Hermes”
    build-name=”Hermes-GatedCI”
    build-number=”Hermes-GatedCI_20140123.10″
    drop-directory-subfolder=”_PublishedWebsites”
    to-folder=”hermes”
    />
  <build-output
    description=”Scheduling”
    team-project=”Scheduler”
    build-name=”QA-build”
    build-number=”QA-build_20130302.9″
    drop-directory-subfolder=”_PublishedWebsites”
    to-folder=”scheduler”
    />
</composed-build>

The Composed Build DLL

The configuration file is useless without something that knows how to read it and perform the logic of talking to Team Foundation Server and copying the appropriate files to the build server’s disk.  This logic is wrapped inside of a DLL named Benday.ComposedBuild.Core.dll.  The main entry point to this DLL is a class named ComposedBuildManager.  The ComposedBuildManager class has an Initialize() method that takes the contents of the config file, the URL for the TFS Team Project Collection (TPC), and the working directory where the individual build outputs should be copied to. 

The New Optional Pre-Build Script

You can find the new pre-build and post-build script options on the Process tab under Build –> Advanced process parameters for the Default Template (TfvcTemplate.12.xaml) as shown below.  These new options allow you to run an arbitrary PowerShell script during your build.  The intent here is to allow you a simple way to customize your TFS Builds without having to open the XAML Workflow editor.  (BTW, you can also do the same thing with pre-test and post-test, too.) 

image

The PowerShell script that I created for the composed build’s pre-build event takes two parameters.  One that contains the TFS TPC URI (example: http://my-tfs:8080/tfs/DefaultCollection) and another that contains the subdirectory that should be created off of the TFS Build’s sources directory.  The script has access to the TFS Build’s source dir value for the current build by accessing the TF_BUILD_SOURCESDIRECTORY environment variable using $Env:TF_BUILD_SOURCESDIRECTORY. 

# PreBuildScript.ps1
# Enable -Verbose option
[CmdletBinding()]
   
# Disable parameter
# Convenience option so you can debug this script or disable it in
# your build definition without having to remove it from
# the ‘Post-build script path’ build process parameter.
param([switch]$Disable, [string]$tfsUri=””, [string]$workingSubDir=””)
if ($PSBoundParameters.ContainsKey(‘Disable’))
{
    Write-Verbose “Script disabled; no actions will be taken on the files.”
}

Write-Verbose “tfsUri: $tfsUri”
Write-Verbose “workingSubDir: $workingSubDir”

$sourceDir = $Env:TF_BUILD_SOURCESDIRECTORY, $sourceSubDir
Write-Verbose “sourceDir: $sourceDir”

# where is the config file for the composed build
$configFilePath = [System.IO.Path]::Combine($sourceDir, “composed-build-config.xml”)
Write-Verbose “configFilePath: $configFilePath”

# path for the composed build DLL
$composedBuildBinaryPath = [System.IO.Path]::Combine($sourceDir, “BuildBinaries”, “Benday.ComposedBuild.Core.dll”)
Write-Verbose “composedBuildBinaryPath: $composedBuildBinaryPath”

# working directory path where the referenced build outputs
# will be downloaded to. 
$workingDir = [System.IO.Path]::Combine($sourceDir, $workingSubDir)
Write-Verbose “workingDir: $workingDir”

if (![System.IO.Directory]::Exists($workingDir))
{
    [System.IO.Directory]::CreateDirectory($workingDir)
}

# load the composed build assembly from the DLL
[System.Reflection.Assembly]::LoadFrom($composedBuildBinaryPath)

# create an instance of the ComposedBuildManager
$downloader = new-object Benday.ComposedBuild.Core.ComposedBuildManager

# read the contents of the config file
$configFileContents = [System.IO.File]::ReadAllText($configFilePath)
Write-Verbose “config contents: $configFileContents”

# initialize the ComposedBuildManager
$downloader.Initialize($configFileContents, $tfsUri, $workingDir)

Write-Verbose “Starting download…”
$downloader.Download()
Write-Verbose “Download completed.”

The PreBuildScript.ps1 assumes that the configuration file (composed-build-config.xml) is in the root of the source dir ($Env:TF_BUILD_SOURCESDIRECTORY).  It also assumes that Benday.ComposedBuild.Core.dll has been added to source control in a subfolder of the source dir named BuildBinaries. 

The TFS Composed Build Definition

By using the new pre-build script to customize our build, we can completely avoid customizing the Windows Workflow XAML of the TFS build script.  I’ve added the PowerShell build script (PreBuildScript.ps1) to TFS Source Control in a directory named BuildScripts.  From the Build process parameters editor, I set the Pre-build script path to point to the PreBuildScript.ps1 file in source control. 

SNAGHTML158af32

Then set the Pre-build script arguments.  Here are the pre-build script arguments:
  -Verbose -tfsUri http://demotfs2013:8080/tfs/DefaultCollection -workingSubDir downloadHere 

The “-Verbose” enables verbose logging and this allows all the Write-Verbose calls in the PowerShell script to show up in the TFS build log.  The “-tfsUri” argument is required because it supplies the TFS Team Project Collection URI to the PowerShell script.  The “-workingSubDir” directory is optional and allows you to specify a subdirectory off of the build’s source dir where the ComposedBuildManager class will copy the build outputs.  If you don’t specify the “-workingSubDir” value, ComposedBuildManager creates build output directories directly off of the source dir. 

Once you’ve done the above steps, you’re finished and all you need to do is run the composed build definition.  Voila!  The ComposedBuildManager talks to TFS, resolves the UNC path for the drop directory for each referenced build, and then copies the bits from the drop directory to the local working directory on the TFS Build machine.  Once you’ve got these on disk, you can package them up and/or deploy them as needed. 

You can download the source code and some sample configuration files from here.

I hope this helps you out with your tricky build situations.

-Ben

 

— Got a difficult Team Foundation Server build that you need help with?  Want some training getting going with TFS and TFS Builds?  Trying to figure out how to help streamline your Scrum process with TFS Builds?  We can help.  Drop us a line at info@benday.com


One Response to "Howto: Compose a TFS build from the output of previous builds with a configuration file"
  1. Atul says:

    Great help! I was about to die with the problem I was facing and then you saved me! SuperMan!