Thursday, May 27, 2010

Setting up a custom Team Build project

Our company has Team Server 2008 and Team Build agents all set up and ready to go, except that no one really knows what to do with it. We don't have unit tests and we don't do code analysis and our deployment process is quite manual. And we haven't really established what the Microsoft compatible method is for organizing our version control repository.

Microsoft products are very "in the box". By that I mean, if you are going to do what Microsoft envisioned you would do then everything is very easy (if you can figure out what it is they envisioned). As soon as you step outside the box you're in for a world of pain.

As with many Microsoft tools it seems that in order to understand the tool have to first understand the tool. So, with this post, I hope to help some other newbies get started.

So, to help ease the pain (but while realizing this is the blind leading the blind) here are some get started tips.

Our scenario was to simply pull the latest version a website from TFS and deploy it to a test web server. We wanted this to happen automatically when a check-in occurred. As simple as this sounds it is not "in the box".

The box is this:
  1. Optionally get the latest code from TFS (with options for incremental get, cleaning, overwritting, etc).
  2. Optionally clean the build
  3. Optionally modify the default Drop location format
  4. Build
  5. Optionally Test
  6. Optional Code Analysis
  7. Semi-optionally drop the build to a network share
I am assuming you've already set up a TFS server and a TFS Build Agent on the same or a different server. I didn't have to do that and don't know anything about it.

All the configuration is handled inside Visual Studio which remotely configures the Build Server. If you are using TFS 2008 then you must use Visual Studio 2008 (with the Team Explorer component installed). If you are using Visual Studio 2010 with TFS 2008 you will not be able to "Manage Build Agents" which is a required piece.

Step 1: Set up the Build Agent (this all happens in Visual Studio)
  • Using the Build menu select Manage Build Agents
  • Give any display name and description
  • Enter the computer name where you installed the TFS Build Agent service
  • Enter the port (it'll probably be the default)
  • The working directory is the location out on the computer were the TFS Build Agent is installed where all the temporary files will be placed (source code, logs, build output, etc). This includes a TFS MSBuild variable thingy called $(BuildDefinitionPath). Leave this in your path so the files in this build stay separated from other builds. This variable is assigned the name of the Build Definition that you create later.
  • They give you a bunch of warnings about having sufficient disk space (since in their scenarios builds take hours and a failed build is the worse thing possible).
  • Set the Agent to Enabled.
One Build Agent can only serve a single TFS server but can build lots of different projects from that TFS server.

Step 2: Build Definition

The build definition links together the source control path (so it knows what files to get), the build agent and the network share where it drops the output files. It also helps you create a basic TFSBuild.proj project file (it's just XML) where you customize what happens in the automated build.

In our environment we pretty much only use the Source Control portion of TFS (we don't use Work Items, Reports or Documentation). So I have mental blinders when I connect to the TFS server in Visual Studio (Team Explorer) and always immediately open the Source Control window without ever looking at the other items. One of those items is Builds. This is where you define your Build Definitions.

Right click on Builds and choose New Build Definition from the context menu. This gives you the Build Definition Dialog:
  • Give the Build Definition a name
  • Defined a workspace. My initial workspace was full of unrelated items. Delete everything you don't need as part of the build and add only those you need. Or, you can use the Copy Existing Workspace to pull in one that you are already using in Visual Studio
  • Next create a project file. Project files are stored and executed from within TFS. So create a place in TFS to store the project files. We store ours in a separate location from our projects (a separate TeamBuild folder with a subfolder for each Build Definition). Use the Create... button to get a default TFSBuild.proj file created for you. Here you choose which solution to build. Choose your tests and code analysis. Our project didn't use any of these so you're one your own. Once you've finished the Create... wizard new TFSBuild.proj and TFSBuild.rsp files will be created in the location you specified. This TFSBuild.proj file is where you customize your build.
  • Next choose your retention policy. The output from each is kept. During your initial setup you might want to keep everything so you can review the logs. You can go back and revisit what you want to keep after everything is finished. In the end you'll probably only want to keep Failed or Partially Succeeded builds so you can review the logs.
  • Next choose the Build Agent you setup in the last step. You are also required to choose a network share where the build will be "dropped" (a place where the result will be copied to). For some reason it MUST be a network share. Also, the user you configured the Team Build Service to execute as must have Full Permission access to that share. Also, the build agent will automatically create a drop folder on the share you specify (so you can use one share for lots of different Build Definitions).
  • Choose your desired trigger. During your initial setup you probably want to select "Check-ins do not trigger a new build". The amounts to "Manual Build". This way you can tweak your automated build and manually execute it rather than being tied to some other build trigger. Later you can return and select the most appropriate option.
Now you've got a build definition. And if you just want to Build, Test, etc. you are probably done. In our case we don't want to build at all. We want to get the latest from source control and simply copy files out to the Drop location.

Step 3: Customizing the TFSBuild.proj

In order to make the Build Server do something other than the default we need to break open the TFSBuild.proj file. Without some kind of guidance this can be a nebulous void of XML. However, the project file is what is executed and little is hidden (even if nothing is obvious).

Here are some tips for dealing with this file:
  • There are some DO NOT EDIT things there and, probably, you can just ignore that.
  • There are some Backwards Compatibility lines in there. And unless you are using old TFS stuff then you can delete that (so it's not in the way). Most of that legacy stuff is now defined in the Build Definition rather than the TFSBuild.proj file.
  • Think of this file like a class file that is inheriting from another class. That parent class is Microsoft's build instructions (get from TFS, build, clean, test, drop, etc). You get all the default functionality and can override any part of it to do what you want
  • Remember that when you override something you must still accept the same inputs and provide the proper outputs if you want everything to work right in the overall system (for example, there is a specific action that you have to take if you want to indicate that the build process was successful. If you don't it will report failed even if you didn't have any errors).
I had trouble finding a list of all the things that I could override and what variables and things were available (MSDN has a lot of this info). Fortunately you can look at the "parent class" source. It is just another XML MSBuild project file. And your TFSBuild project file includes it right near the top.

It's path is: $(MSBuildExtensionsPath)\Microsoft\VisualStudio\TeamBuild\Microsoft.TeamFoundation.Build.targets

This translates to c:\Program Files\MSBuild\Microsoft\VisualStudio\TeamBuild\Microsoft.TeamFoundation.Build.targets in our environment.

Everything Team Build does is (pretty much) in that file. If you don't overwrite anything in your TFSBuild.proj file then you're looking at the code that will be executed.

There are still a lot of non-obvious things going on here. You'll see a lot of references to DesktopBuild. That is just there to throw you off. Also, since you're looking at XML and not a procedural language the execution order is not defined by the order items appear in the file.

A couple of items the differ in a TFS build vs a regular build are:
  • Entry Points
  • Variables
TFS starts by calling the Target named "EndToEndIteration". This target has several dependencies defined on it so those are executed first. In fact, EndToEndIteration doesn't actually have any code to execute. It just ensures that all its dependencies are executed in the proper order. The dependencies are listed above the Target definition (I think this is only by convention). These are the names of the several Targets that should be executed before executing the current Target.

You can override EndToEndIteration to make your own fully customized build process but since you are working in a TFS build environment you'll probably still want to execute several of the existing dependecies. InitializeBuildProperties, for example, is important as it imports a bunch of TFS settings (like paths, TFS Source Control URLS, etc) into the build so you can work from those.

Also, some activities that you will want to execute are already available. So get familiar with what is there (e.g., the "get" target pulls down the files from source control).

Defining/Overriding variables

There are two kinds of variables in these build files. The most common are defined inside PropertyGroup tags. The other kinds are called Items and I'm not sure what the difference is.

You can define your own variables like so:
<propertygroup>
<myvariable>The Value</myvariable>
<myvariable2>The Value of Var #2</myvariable2>
</propertygroup>

Later you can dereference the variables using the $() syntax:

<propertygroup>
<myvariable3>$(MyVariable) of Var #3</myvariable3>
</propertygroup>

Defining/Overriding Targets

You define (or override existing) targets using the Target tag. Your own Targets need a unique name. Override an existing target by using the existing target name. Microsoft's pre-defined targets include several that are intended to be overridden.

<target name="MyTarget">
<message text="This is my target">
</target>

A Target can hold more variables (PropertyGroup) and call other actions. Above I call the Message action. There are lots of actions available. See MSDN or Google to get some lists.

In MS' default configuration most Targets have a before and after Target you can override to do whatever you need.

Greater Customization

In our project we didn't want to build anything. We just wanted some files copied out to a development server whenever someone checked in a change. This was too outside the box to do with the default configuration.

So I ended up with the following override of EndToEndIteration to do what I wanted:

<propertygroup>
<endtoenditerationdependson>
CheckSettingsForEndToEndIteration;
InitializeBuildProperties;
InitializeEndToEndIteration;
InitializeWorkspace;
Get;
DeployWebFiles;
Messages;
</endtoenditerationdependson>
</propertygroup>
<!-- Entry point: this target is invoked on the build machine by the build agent -->
<Target Name="EndToEndIteration"
DependsOnTargets="$(EndToEndIterationDependsOn)" />


You can see that it includes several of the initialization Targets but then skips to Get and then to my own Targets.

My targets look like this:

<propertygroup>
<deploywebfilesdependson>
</propertygroup>
<Target Name="DeployWebFiles"
DependsOnTargets="$(DeployWebFilesDependsOn)">

<BuildStep TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
BuildUri="$(BuildUri)"
Name="DeployWebFiles"
Message="Deploying Web Files">
<output taskparameter="Id" propertyname="DeployWebFilesBuildStepID">
</buildstep>

<itemgroup>
<filestocopy include="$(SolutionRoot)\NET 1.1\Trunk\Source\Web\**\*">
</itemgroup>
<Copy
SourceFiles="@(FilesToCopy)"
DestinationFiles="@(FilesToCopy ->'$(DropLocation)\%(RecursiveDir)%(Filename)%(Extension)')"
SkipUnchangedFiles="true"
ContinueOnError="false" />

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

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

<onerror executetargets="PartialSuccess">

</target>

<target name="PartialSuccess">
<BuildStep TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
BuildUri="$(BuildUri)"
Id="$(DeployWebFilesBuildStepID)"
Status="Failed"
/>

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


I found examples of copying files through Google. I had to experiment a little to discover the file structure. You can browser the folders on your Build Agent system (remember step 1?).

Setting the right outputs

Visual Studio/TFS needs certain outputs in order to understand what is going on inside your custom targets.

I used the BuildStep action to display more steps that I can see inside Visual Studio's Build Explorer so I can watch the live progress of my build. TFS automatically does some of its own build steps but you can add as many as you want. You first create a new build step and accept its output of a Build Step ID (which you save in a variable). Then you can update that Build Step's status by passing the ID (using the $() syntax) in again.

The last tricky item is getting the build to report as successful. I finally got everything working with no errors or warnings but Visual Studio still reported the build as failed.

It looks like when you allow some of MS' default targets to run it somehow updates the build status (even though I can't find where). But, to handle this manually by calling the SetBuildProperties action.

We have to set the CompilationStatus and TestStatus to the string "Succeeded" in order to get TFS to believe that the build was successful. Again, here we are outside the box and even though we aren't doing a compilation or a test we have to report them as successful.

Drop Location

The last hiccup we had was with the Drop Location. All of MS' code turns the drop location from the one we specified in the Build Definition and adds a build path to it:

//myserver/myshare/BuildDefinitionName, Date.Number/

In our case we didn't want any of that. This meant we could either overwrite the DropBuild target or we could role our own Target. Since DropBuild is meant to deploy the OUTPUT of a build and not the source files I decided to role my own. Probably in the future we will use the DropBuild target and I didn't want to confuse other team members as to what the DropBuild functionality really is supposed to be.

The final hiccup that we haven't been able to resolve is that somewhere in the whole build process the Build folder is still generated in the Drop Location and the log file is placed inside it. I'm not sure if this is a function of the TFS Build service or one of the actions called by MS' build XML code. But it is really minor and we probably won't worry about it.

The End

I hope this helps get someone started. Unless you find a good book on the subject this initial hurdle is pretty difficult to get over. Most of the documentation/books I found were much more advanced and followed the original rule that in order to understand TFS Builds you have to first understand TFS builds.

No comments:

Post a Comment