Welcome to MSDN Blogs Sign in | Join | Help

With TFS Build 2010 (Beta 1), we've changed the build orchestration language from MSBuild to Windows Workflow Foundation (WF).  As such, you now have a new option for adding custom logic to your build proces - custom WF activities.  I've gotten a couple of requests for a blog post with an example, best practices, and so forth, so here goes.

There are four ways (that I know of, at least) to author a custom WF activity - using the WF Designer / XML Editor and composing your custom activity in XAML; composing your custom activity in code; writing a custom CodeActivity; or writing a custom NativeActivity.  This list is more or lest organized by degree of difficulty, and more or less by preference as well. 

The first two approaches involve building a new activity up from existing activities - when possible, this is a good strategy, since it has several advantages over writing custom code to do your work:

  1. Composed activities obviously re-use code to a high degree, which is always a good idea - why reinvent the wheel if you don't have to?
  2. Composed activities are by their nature cancellable by the workflow runtime, meaning that builds running your custom activity will be easy to stop cleanly.
  3. Composed activities can participate in workflow tracking, meaning that their internal progress can be easily tracked as they execute.  (If you don't want their progress to be tracked when running in a build, you can turn this behavior off using Microsoft.TeamFoundation.Build.Workflow.Tracking.ActivityTrackingAttribute.)
  4. It's comparatively easy, at least once you get the hang of it.

I'll be tackling these approaches in this post, and the CodeActivity and NativeActivity approaches in a subsequent post.  The activity I'll be writing takes in a server path to a script file and a Workspace object (along with a couple of other optional parameters), executes the script, and returns the exit code.  The assumption, then, is that this activity will be used in a build process at some point after the workspace for the build has been set up and after that workspace has been synched.  Note that a big difference between MSBuild and WF is that WF can hand actual objects around between activities, as opposed to MSBuild which basically just hands strings around. 

To write my activity, I created a new WF ActivityLibrary project, added references to Microsoft.TeamFoundation.Build.Client, Microsoft.TeamFoundation.Build.Common, Microsoft.TeamFoundation.Build.Workflow, Microsoft.TeamFoundation.VersionControl.Client, and Microsoft.TeamFoundation.VersionControl.Common.  I then added the various arguments I wanted my activity to have using the Arguments flyout in the WF designer, dragged and dropped a Sequence activity and the two TFS Build activities I needed, set the appropriate properties, etc.  Here's what it looks like in the Beta 1 WF Designer:

InvokeScript

And here's the XAML:

<p:Activity mc:Ignorable="" x:Class="ActivityLibrary1.InvokeScript" xmlns="http://schemas.microsoft.com/netfx/2009/xaml/activities/design" xmlns:__InvokeScript="clr-namespace:ActivityLibrary1;" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mtbwa="clr-namespace:Microsoft.TeamFoundation.Build.Workflow.Activities;assembly=Microsoft.TeamFoundation.Build.Workflow" xmlns:mtvc="clr-namespace:Microsoft.TeamFoundation.VersionControl.Client;assembly=Microsoft.TeamFoundation.VersionControl.Client" xmlns:p="http://schemas.microsoft.com/netfx/2009/xaml/activities" xmlns:sad="clr-namespace:System.Activities.Debugger;assembly=System.Activities" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <x:Members>
    <x:Property Name="Arguments" Type="p:InArgument(x:String)" />
    <x:Property Name="Script" Type="p:InArgument(x:String)" />
    <x:Property Name="WorkingDirectory" Type="p:InArgument(x:String)" />
    <x:Property Name="Workspace" Type="p:InArgument(mtvc:Workspace)" />
    <x:Property Name="ExitCode" Type="p:OutArgument(x:Int32)" />
  </x:Members>
  <p:Sequence DisplayName="Invoke Script From Source Control">
    <p:Sequence.Variables>
      <p:Variable x:TypeArguments="x:String" Name="localScriptPath" />
    </p:Sequence.Variables>
    <mtbwa:ConvertWorkspaceItem DisplayName="Convert Server Path to Local Path" Input="[Script]" Result="[localScriptPath]" Workspace="[Workspace]" />
    <mtbwa:InvokeProcess Arguments="[String.Format(&quot;/c &quot;&quot;{0}&quot;&quot; {1}&quot;, localScriptPath, Arguments)]" DisplayName="Invoke the Script" FileName="[&quot;cmd.exe&quot;]" Result="[ExitCode]" WorkingDirectory="[WorkingDirectory]">
      <mtbwa:InvokeProcess.BeforeExecute>
        <p:ActivityAction x:TypeArguments="x:String">
          <p:ActivityAction.Argument>
            <p:Variable x:TypeArguments="x:String" Name="commandLine" />
          </p:ActivityAction.Argument>
          <mtbwa:WriteBuildMessage Message="[commandLine]" />
        </p:ActivityAction>
      </mtbwa:InvokeProcess.BeforeExecute>
      <mtbwa:InvokeProcess.ErrorDataReceived>
        <p:ActivityAction x:TypeArguments="x:String">
          <p:ActivityAction.Argument>
            <p:Variable x:TypeArguments="x:String" Name="errorData" />
          </p:ActivityAction.Argument>
          <mtbwa:WriteBuildError Message="[errorData]" />
        </p:ActivityAction>
      </mtbwa:InvokeProcess.ErrorDataReceived>
      <mtbwa:InvokeProcess.OutputDataReceived>
        <p:ActivityAction x:TypeArguments="x:String">
          <p:ActivityAction.Argument>
            <p:Variable x:TypeArguments="x:String" Name="outputData" />
          </p:ActivityAction.Argument>
          <mtbwa:WriteBuildMessage Importance="Normal" Message="[outputData]" />
        </p:ActivityAction>
      </mtbwa:InvokeProcess.OutputDataReceived>
    </mtbwa:InvokeProcess>
  </p:Sequence>
</p:Activity>

You might notice that the various ActivityActions in the XAML (which handle writing the command-line and standard output to the build activity log as messages, and standard error as errors) are not present in the designer - we didn't get around to implementing a custom designer for the InvokeProcess activity for Beta 1, so for this activity I had to "drop to XAML" and edit the XML directly.  Normally this shouldn't be required - for the most part you should be able to fully author your custom activities in the WF Designer.  When my activity library project is built, this XAML will be compiled into an activity named InvokeScript (the name comes from the x:Class attribute on the root).

As mentioned above, activities can be composed either in xaml or in code.  To write a composed activity in code, you'll derive from System.Activities.Activity, or System.Activities.Activity<T>, add whatever arguments and so forth you need, and then override the CreateBody method to return your composed implementation.  Here's a second version of my InvokeScript activity, written in C# code:

using System;
using System.Activities;
using System.Activities.Statements;
using System.ComponentModel;
using Microsoft.TeamFoundation.Build.Workflow.Activities;
using Microsoft.TeamFoundation.VersionControl.Client;

namespace ActivityLibrary1
{
    public sealed class InvokeScript2 : Activity<Int32>
    {
        [Browsable(true)]
        [DefaultValue(null)]
        public InArgument<String> Arguments { get; set; }

        [Browsable(true)]
        [DefaultValue(null)]
        public InArgument<String> Script { get; set; }

        [Browsable(true)]
        [DefaultValue(null)]
        public InArgument<String> WorkingDirectory { get; set; }

        [Browsable(true)]
        [DefaultValue(null)]
        public InArgument<Workspace> Workspace { get; set; }

        protected override WorkflowElement CreateBody()
        {
            Variable<String> localScriptPath = new Variable<String>();
            Variable<String> commandLine = new Variable<String>();
            Variable<String> outputData = new Variable<String>();
            Variable<String> errorData = new Variable<String>();

            return new Sequence
            {
                Variables = 
                {
                    localScriptPath
                },
                Activities =
                {
                    new ConvertWorkspaceItem
                    {
                        Input = new InArgument<String>(env => this.Script.Get(env)),
                        Workspace = new InArgument<Workspace>(env => this.Workspace.Get(env)),
                        Result = localScriptPath
                    },
                    new InvokeProcess
                    {
                        FileName = new InArgument<String>(env => "cmd.exe"),
                        Arguments = new InArgument<String>(env => String.Format("/c \"{0}\" {1}", localScriptPath.Get(env), Arguments.Get(env))),
                        WorkingDirectory = new InArgument<String>(env => this.WorkingDirectory.Get(env)),
                        Result = new OutArgument<int>(env => this.Result.Get(env)),
                        BeforeExecute = new ActivityAction<String>
                        {
                            Argument = commandLine,
                            Handler = new WriteBuildMessage
                            {
                                Message = commandLine
                            },
                        },
                        OutputDataReceived = new ActivityAction<String>
                        {
                            Argument = outputData,
                            Handler = new WriteBuildMessage
                            {
                                Message = outputData
                            },
                        },
                        ErrorDataReceived = new ActivityAction<String>
                        {
                            Argument = errorData,
                            Handler = new WriteBuildError
                            {
                                Message = errorData
                            },
                        },
                    },
                },
            };
        }
    }
}

The primary difference between this version and the XAML version is that here I've elevated the single output argument (ExitCode) to the return value of the activity - hence it derives from Activity<Int32>.  The metaphor for activities is methods and functions - when you have a single output, the standard practice is to elevate it to the return value rather than using an OutArgument (but this is not, so far as I know, possible when writing an activity in XAML). 

I did a post a while back on a change in TFS Build SP1 which reduced build log noise by cutting out the build steps for project-to-project references (of the form "Project 'project' is building project 'dependent project' for target(s) 'GetNativePath'", and so forth).  Unfortunately, as noted on the TFS Build forums and elsewhere, the fix in SP1 didn't actually fix the problem, at least not completely.  In particular, the mechanism we used to communicate the TargetsNotLogged property to our logger and thereby skip the logging of particular targets only worked the first time a particular project-to-project reference was evaluated.  As such, if a project was referenced by 10 other projects the fix in SP1 would reduce the number of spurious build steps from 10*3 to 9*3 rather than removing them altogether.

A hotfix is finally available that will address the remaining parts of the problem - it can be downloaded here.  Note that this hotfix assumes that you have already installed SP1 on your build machine, so make sure to install SP1 and then the hotfix if you are currently running TFS Build 2008 RTM bits.  After this hotfix is applied you should, by default, never see another project-to-project reference build step - I've heard reports from some customers of this reducing their total number of build steps from over 3000 to under 200, so in some cases this can have a dramatic effect on build time (each of those build steps corresponds to a server call made during the build).

I've done a couple of posts now on calling custom targets during a Team Build build - this one focused on calling custom targets in the solution/project being built, and this one focused on calling custom targets in your TfsBuild.proj file itself before/after the compilation of individual solutions or configurations.  In the past week I've gotten two questions on whether one can call custom targets before/after the compilation of individual projects within a solution.  That is, whether Microsoft.TeamFoundation.Build.targets supports Before/AfterCompileProject targets in the same way it supports Before/AfterCompileConfiguration and Before/AfterCompileSolution ones.

The short answer is that this is not really supported in Team Build 2008 (or 2005, for that matter), since by the time your solutions are getting built we have essentially ceded control to MSBuild.  There are three potential methods (that I could think of) for doing this sort of thing:

  1. You could specify the individual projects, rather than the solution that contains them, in the SolutionToBuild item group in your TfsBuild.proj file and then just use the Before/AfterCompileSolution targets to execute your custom logic.  This is only appropriate if your solutions are simple containers for your projects - in particular, if you don't have build order logic and/or project-to-project references in your solution that you would need to re-implement in your TfsBuild.proj file.
  2. You could use a custom MSBuild "traversal" project in place of your solution, and insert your custom logic into this traversal project before/after the individual projects are compiled.  The simplest thing here would be to start with the the project that MSBuild generates from your solution - in MSBuild 3.5 this project will be written to disk as a *.sln.cache file if you build your solution from the command-line.  The disadvantage to this approach is that you then have two files to maintain in parallel - your solution and your custom traversal project.  One possible workaround here would be to write a custom MSBuild task to generate this traversal project automatically as part of the build.  If anybody is adventurous enough to attempt this task and generous enough to share, please let me know and I'd be happy to post your efforts in a follow up. 
  3. Finally, you could put the before/after compile logic into the individual projects themselves, either directly or via in imported *.targets file.  Individual .NET projects have their own Before/AfterBuild extensibility targets that could be utilized for this purpose. 

This last option strikes me as the best one when including simplicity in the calculation...  The targets file would look something like this:

<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

  <!-- Only execute this custom logic when the project is building under Team Build. -->
  <Target Name="AfterBuild" Condition=" '$(TeamBuildConstants)' == '_TEAM_BUILD_' ">
    <!-- Put your custom logic here. -->
  </Target>
  
</Project>

You would then check this targets file in alongside your TfsBuild.proj file and import it into your individual projects after the import of Microsoft.Common.targets using a relative path.  For example, the end of a *.csproj file might look like:

  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
  <Import Project="..\..\TeamBuildTypes\SomeDefinition\AfterCompileProject.targets"/>
</Project>

I've mentioned the CompilationOutputs item group we added in TFS 2008 before in passing (see this post, for example), but never given it the attention it deserves...  This item group is built up over the course of the Compile / CompileConfiguration / CompileSolution targets, and by the end of the build it contains the full list of outputs generated by all the items in your SolutionToBuild item group.  Furthermore, it includes metadata that allows you to distinguish the outputs for each platform/configuration pair and for each individual solution.  For example, given solutions Foo.sln and Bar.sln and configurations x86|Debug and x86|Retail, the following target:

  <Target Name="AfterCompile">
    <Message Text="%(CompilationOutputs.Solution) built %(Identity) for %(Platform)|%(Configuration)." /> 
  </Target>

...might produce the following output:

  C:\BuildLocation\TeamProject\Definition\Sources\Foo.sln built C:\BuildLocation\TeamProject\Definition\Binaries\x86\Debug\Foo.exe for x86|Debug.
  C:\BuildLocation\TeamProject\Definition\Sources\Bar.sln built C:\BuildLocation\TeamProject\Definition\Binaries\x86\Debug\Bar.exe for x86|Debug.
  C:\BuildLocation\TeamProject\Definition\Sources\Foo.sln built C:\BuildLocation\TeamProject\Definition\Binaries\x86\Release\Foo.exe for x86|Release.
  C:\BuildLocation\TeamProject\Definition\Sources\Bar.sln built C:\BuildLocation\TeamProject\Definition\Binaries\x86\Release\Bar.exe for x86|Release.

Of course, producing that output is not particularly exciting.  It is easy to imagine lots of other cool things that can be done with this information, however, including copying all build outputs to the drop location without putting them in the default build location (where they all end up in a single folder).  I'm sure users of Team Build will come up with lots of other creative uses for this item group as well.

A fair number of people seem to want to use Team Build to kick off their own pre-existing build scripts that have nothing in common with the process defined in Microsoft.TeamFoundation.Build.targets.  While we don't typically encourage this, enough people want to do it that I figured I should finally post an example.  So - the minimal tfsbuild.proj file that will kick off your custom build script would look something like this:

<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="3.5">
  <Target Name="EndToEndIteration">
    <Exec Command="SomeScript.cmd" />
  </Target>
</Project>

Of course, most of the Team Build goodness comes from executing the standard targets while having our logger attached, so this approach will not generate a lot of information - no build steps will show up in the build report within VS, it won't keep track of errors/warnings, no data will be pushed to the warehouse (and thus reporting across builds will not work), and so forth.

If you can abstract your custom build script down to the compilation portion, and let the standard build process handle the Get, the Label, etc. you'll be in better shape.  To do this, just override the BeforeCompile target and execute your custom script there, leaving the rest of the TfsBuild.proj file (especially the import of Microsoft.TeamFoundation.Build.targets) alone.

We got a forum post the other day on whether changesets and work items could be associated since the last Successful build.  Some of you may be thinking "Isn't that how it already works?"  Actually, it's not quite how it works.  Changesets and Work Items are, by default, associated since the last "good" build, where "good" means compilation and tests succeeded, but something else may have gone wrong.  That is, the last "good" build may well be marked Partially Succeeded and not Succeeded.

I started responding to the forum post, and then figured it would make a good blog post instead.  (Of course, I've posted a link to this post in the forums...)

So - to modify the default behavior and associate changesets and work items only since the last successful build, you'll need to do three things:

  1. Skip the default logic to associate changesets and work items.  This is pretty simple - just set the SkipGetChangesetsAndUpdateWorkItems property to true.
  2. Write a custom task to find the last successful build and return its label.
  3. Call this custom task and then call the GenCheckinNotesUpdateWorkItems task with its output.

Lucky for you, I've already written the custom task (or at least a simple version of it).  Here is the source for it:

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

namespace BlogProjects
{
    public class GetLastSuccessfulBuildLabel : ITask
    {
        #region Properties

        [Required]
        public String BuildDefinitionName
        {
            get
            {
                return m_buildDefinitionName;
            }
            set
            {
                m_buildDefinitionName = value;
            }
        }

        [Output]
        public String LastSuccessfulBuildLabel
        {
            get
            {
                return m_lastSuccessfulBuildLabel;
            }
        }

        [Required]
        public String TeamFoundationServerUrl
        {
            get
            {
                return m_teamFoundationServerUrl;
            }
            set
            {
                m_teamFoundationServerUrl = value;
            }
        }

        [Required]
        public String TeamProject
        {
            get
            {
                return m_teamProject;
            }
            set
            {
                m_teamProject = value;
            }
        }

        #endregion

        #region ITask Members

        public bool Execute()
        {
            TeamFoundationServer tfs = TeamFoundationServerFactory.GetServer(TeamFoundationServerUrl);
            IBuildServer buildServer = (IBuildServer)tfs.GetService(typeof(IBuildServer));

            IBuildDetailSpec spec = buildServer.CreateBuildDetailSpec(TeamProject, BuildDefinitionName);
            spec.MaxBuildsPerDefinition = 1;
            spec.Status = BuildStatus.Succeeded;
            spec.QueryOrder = BuildQueryOrder.FinishTimeDescending;

            IBuildQueryResult queryResult = buildServer.QueryBuilds(spec);

            if (queryResult.Builds.Length > 0)
            {
                m_lastSuccessfulBuildLabel = queryResult.Builds[0].LabelName;
            }

            return true;
        }

        public IBuildEngine BuildEngine
        {
            get
            {
                return m_buildEngine;
            }
            set
            {
                m_buildEngine = value;
            }
        }

        public ITaskHost HostObject
        {
            get
            {
                return m_taskHost;
            }
            set
            {
                m_taskHost = value;
            }
        }

        #endregion

        #region Private Members

        private IBuildEngine m_buildEngine;
        private ITaskHost m_taskHost;
        private String m_teamFoundationServerUrl;
        private String m_buildDefinitionName;
        private String m_teamProject;
        private String m_lastSuccessfulBuildLabel;

        #endregion
    }
}

Note the fancy querying that is possible with an IBuildDetailSpec - pretty cool... 

Calling the custom task should be similarly simple.  Something like the following should do the trick:

  <Target Name="BeforeGetChangesetsAndUpdateWorkItems">

    <GetLastSuccessfulBuildLabel TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
                                 TeamProject="$(TeamProject)" 
                                 BuildDefinitionName="$(BuildDefinitionName)">
      <Output TaskParameter="LastSuccessfulBuildLabel" PropertyName="LastSuccessfulBuildLabel" />
    </GetLastSuccessfulBuildLabel>

    <GenCheckinNotesUpdateWorkItems TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
                                    BuildUri="$(BuildUri)"
                                    BuildNumber="$(BuildNumber)"
                                    CurrentLabel="$(LabelName)@$(LabelScope)"
                                    LastLabel="$(LastSuccessfulBuildLabel)"
                                    UpdateWorkItems="$(UpdateAssociatedWorkItems)"
                                    ContinueOnError="true" />

  </Target>

Hope this helps!

UPDATE: My original post had a typo - the property SkipGetChangesetsAndUpdateWorkItems was missing its And.  My apologies to anyone who banged their head against the wall trying to figure out why it wasn't working!

In Orcas, we introduced a generic information storage for builds - internally this is used for all build steps, associated changesets/workitems, etc.  You can use it to attach arbitrary data to a build (and later retrieve it).  Here are a couple of quick code snippets to illustrate these two cases.

To attach a single name/value pair to a build, you would do something like this:

        TeamFoundationServer tfs = TeamFoundationServerFactory.GetServer(tfsUrl);
        IBuildServer buildServer = (IBuildServer)tfs.GetService(typeof(IBuildServer));
        IBuildDetail buildDetail = buildServer.GetBuild(buildUri);

        IBuildInformationNode node = buildDetail.Information.CreateNode();

        node.Type = "MyCompany.Custom";
        node.Fields["key"] = "value";

        buildDetail.Information.Save();

To retrieve the same name/value pair from the build, you would do something like this:

        TeamFoundationServer tfs = TeamFoundationServerFactory.GetServer(tfsUrl);
        IBuildServer buildServer = (IBuildServer)tfs.GetService(typeof(IBuildServer));
        IBuildDetail buildDetail = buildServer.GetBuild(buildUri);

        List<IBuildInformationNode> nodes = buildDetail.Information.GetNodesByType("MyCompany.Custom");

        if (nodes.Count > 0)
        {
            foreach (IBuildInformationNode node in nodes)
            {
                String value = node["key"];

                // Do something...
            }
        }

Note that the information node storage is hierarchical, so you can get a lot fancier than this if you want/need to!  Note that full documentation of the Orcas TFS Build Object Model can be found here.

The official Orcas RTM OM documentation was posted a while ago, and I neglected to blog about it!  It's available here: http://www.microsoft.com/downloads/details.aspx?FamilyID=6466b53d-d80b-4c31-8f5c-dfb5d32e9411&DisplayLang=en

Make sure to follow the instructions for viewing it after downloading:

Method 1

  1. Double-click the .chm file.
  2. In the Open File-Security Warning dialog box, clear the Always ask before opening this file check box.
  3. Click Open.
Method 2
  1. Right-click the CHM file, and then click Properties.
  2. Click Unblock.
  3. Double-click the .chm file to open the file.

I'm yanking the Beta 2 documentation now that this has been posted...

If you are planning on attending Tech*Ed Developers in sunny Orlando this year, I'll be manning the Visual Studio Team System 2008 Team Foundation Server (Version Control and Build) demo station (quite the name, eh?) for about 22 hours over the course of the four day event.  Please stop by and say hello, ask some questions, provide some feedback, etc.

Other folks at the booth will include my fellow Visual Studio North Carolina folks Ed Hintz (a man of many hats, including Dev Lead for the Version Control Client and Team System Web Access teams and Power Tools guru), and Chris Rathjen (tester extraordinaire on the Admin & Ops team).

Hope to see lots of you there!

As promised, here are some more details on other SP1 changes for TFS Build. 

3. Detect test results.

In Whidbey, a failed test would result in a failed build - builds were either Succeeded or Failed, so there wasn't really much middle ground.  In Orcas we added two new build status properties, CompilationStatus and TestStatus; and a new value for the overall build status - PartiallySucceeded.  The overall status of the build would be Succeeded if an only if there were no errors of any kind during the build; PartiallySucceeded if no errors occurred before or during compilation; and Failed otherwise.  If a test failed, CompilationStatus would be succeeded, TestStatus would be failed, and the overall Status would be partially succeeded.

Many users wanted a bit more control, and in particular wanted a failed test to result in a Failed build.  I posted a workaround that allowed this here, but it was rather hacky - the idea was to set CompilationStatus to failed when a test failed and then let the default logic kick in and fail the build.  In SP1 we introduced a new property - TreatTestFailureAsBuildFailure - that will cause a test failure to fail the build without having to modify CompilationStatus.  Just set this property to true and the new default logic will be to mark the overall status of the build as PartiallySucceeded if no errors occur before or during testing.  That is, if a test fails, CompilationStatus will be succeeded, TestStatus will be failed, and the overall Status will be failed.  Note that partially succeeded builds are still possible when errors occur after successfully running unit tests (e.g. when copying binaries to the drop location).

4. Dynamically created properties.

This one is pretty confusing, and requires an understanding of how the MSBuild task works, an understanding of the Team Build targets file, etc.  I'll describe the problem first, then the solution, and then for anybody who feels like sticking around just a bit about why this problem exists.

The Problem

The first time I heard about this issue, a user was dynamically generating a build number in the BuildNumberOverrideTarget in the standard fashion illustrated here, along with a corresponding version number with which to stamp assemblies.  He was then, however, attempting to actually use that dynamically generated version number during the compilation of his individual solutions.  That is, he had something like:

    <SolutionToBuild Include="SomeSolution.sln">
      <Properties>VersionNumber=$(VersionNumber)</Properties>
    </SolutionToBuild>

...where $(VersionNumber) was getting generated dynamically in BuildNumberOverrideTarget.  Unfortunately, the value getting passed into the individual solutions was the original value of the property - namely, the empty string.  (Note that this same issue would apply if he had attempted to access the VersionNumber property in one of the Before/AfterCompileConfiguration or Before/AfterCompileSolution targets.)

The Solution

The fix is to put all dynamically generated properties into the CustomPropertiesForBuild property (or the CustomPropertiesForClean property if you need to access the property in Before/AfterCleanConfiguration and/or Before/AfterCleanSolution).  For example, the user above could do something like:

  <Target Name="BuildNumberOverrideTarget">
    <BuildNumberGenerator>
      <Output TaskParameter="BuildNumber" PropertyName="BuildNumber" />
      <Output TaskParameter="VersionNumber" PropetyName="VersionNumber" />
    </BuildNumberGenerator>
    <PropertyGroup>
      <CustomPropertiesForBuild>$(CustomPropertiesForBuild);VersionNumber=$(VersionNumber)</CustomPropertiesForBuild>
    </PropertyGroup>
  </Target>

The Description

So what the heck is going on here anyways!?  Well, the issue revolves around the mechanism used by Team Build to support multiproc MSBuild.  In particular, the unit of parallelism in MSBuild is the project, meaning that to build configurations and solutions in parallel Team Build has to use the MSBuild task to call back into TfsBuild.proj for each configuration/solution combination - hence the Compile/CompileConfiguration/CompileSolution targets. 

Furthermore, when a project is invoked with the MSBuild task, all of the environment from the caller (properties, item groups, etc.) is lost - only those properties passed in via the Properties property of the MSBuild task (along with any global properties - see my blog post here for details) are available in the new context.  Since Team Build cannot know the full list of dynamically generated properties, none are passed into these recursive calls to TfsBuild.proj.  Or at least, that's how it used to work...  In SP1, we go ahead and pass the CustomPropertiesForBuild property into the first of these recursive calls, after which any dynamically generated properties (placed into this container) become global and are available for the rest of the chain.

Brian Harry put up a post on the improvements that will be available in the upcoming Team Foundation Server 2008 SP1 release.  Here's some more in depth info on two of the TFS Build changes:

1. Conditionalize builds on the trigger.

There are actually a few changes here...  Essentially we exposed a property on IBuildDetail called Reason that tells you why the build was started.  Reason is an enum of type BuildReason, and can have the following values:

  • Manual.  This indicates that the build was manually started (e.g. by a user through the Queue Build dialog).
  • IndividualCI.  This indicates that the build was started due to a checkin, and that it's build definition is set up to build on each checkin.
  • BatchedCI.  This indicates the the build was started due to one or more checkins, and that it's build definition is set up to accumulate checkins.
  • Schedule.  This indicates that the build was started due to the time, and that it's build definition is set up to build on a regular schedule if changes have occurred.
  • ScheduleForced.  This indicates that the build was started due to the time, and that it's build definition is set up to build on a regular schedule whether or not changes have occurred.

In addition to exposing this new property on IBuildDetail, we have also exposed it as an output property of the GetBuildDetails task and as an MSBuild property available in your TfsBuild.proj files.  This could be useful in a couple situations:

  • You can use it to detect whether a build of an individual definition is running as a result of being triggered by the system or as a result of being started manually by a user.  It may be useful to distinguish these two cases and set various properties to different default values, etc.
  • When pointing more than one build definition at the same TfsBuild.proj file (e.g. so that you can use the same script for your CI build and your Nightly build) you can use it to detect which build definition a particular build is for.  Again, it may be useful to distinguish these two cases.

For example, if you only wanted to generate a custom build number for your nightly build, you could do something like this:

  <Target Name="BuildNumberOverrideTarget" Condition=" '$(Reason)' == 'Schedule' ">
    <MyBuildNumberGenerator TeamFoundationServerUrl="$(TeamFoundationServerUrl)" BuildUri="$(BuildUri)">
      <Output TaskParameter="BuildNumber" PropertyName="BuildNumber" />
    </MyBuildNumberGenerator>
  </Target>

2. Reduce build log noise.

In Orcas RTM, we tried to reduce the dependency of TFS Build on the names of particular targets, tasks, events, etc. in MSBuild.  The goal was to make it more usable by people who wanted to radically customize their build process, and in general I think we did a pretty good job here.  Unfortunately, one side effect of the associated changes was the presence of lots of "noise" build steps in the Build Details dialog.  In particular, each project-to-project reference resulted in three build steps of the form:

  • Project 'project' is building project 'dependent project' for targets 'GetTargetPath'.
  • Project 'project' is building project 'dependent project' for targets 'GetNativeManifest'.
  • Project 'project' is building project 'dependent project' for targets 'GetCopyToOutputDirectoryItems'.

These build steps should all magically disappear when Orcas SP1 is installed. 

To accomplish this, we added a property called TargetsNotLogged to the TFS Build targets file that specifies the target names for which build steps should not be added when our logger receives ProjectStarted events, and made the default value of this property 'GetTargetPath;GetNativeManifest;GetCopyToOutputDirectoryItems'.  If you want these build steps back, just set the property to the empty string in your TfsBuild.proj file.  If you want to exclude other targets as well, just set the property to your desired semicolon-delimited list of excluded targets in your TfsBuild.proj file.

(NOTE: I modified the above text on 12/30/2008 to make it more clear that the TargetsNotLogged property should be modified by overriding the default in your TfsBuild.proj file(s), not by modifying it in place in the TFS Build targets file.)

Next week I'll post on Detect test result and/or Dynamically created properties

In an earlier post I described how one can, in Orcas, preserve the output directory structure used in a standard IDE or desktop build.  It seems that many people are looking for a simple approach, however, to augmenting the standard Team Build output directory by putting the outputs of individual solutions into individual subfolders (I get emails about this pretty regularly).  For this simple case, there is a correspondingly simple solution.  In your TfsBuild.proj file, just replace:

    <SolutionToBuild Include="$(BuildProjectFolderPath)/../../Directory/Solution1/Solution1.sln">
      <Targets></Targets>
      <Properties></Properties>
    </SolutionToBuild>
    <SolutionToBuild Include="$(BuildProjectFolderPath)/../../Directory/Solution2/Solution2.sln">
      <Targets></Targets>
      <Properties></Properties>
    </SolutionToBuild>

...with:

    <SolutionToBuild Include="$(BuildProjectFolderPath)/../../Directory/Solution1/Solution1.sln">
      <Targets></Targets>
      <Properties>OutDir=$(OutDir)Solution1\</Properties>
    </SolutionToBuild>
    <SolutionToBuild Include="$(BuildProjectFolderPath)/../../Directory/Solution2/Solution2.sln">
      <Targets></Targets>
      <Properties>OutDir=$(OutDir)Solution2\</Properties>
    </SolutionToBuild>

Note that this uses the new ability in Team Build 2008 to pass custom properties into each solution when it is built.  As shown in this earlier post, you can also call custom targets for each solution using the Targets metadata also shown in this example.

Recently I got a question about how to compile projects against multiple .NET Frameworks (e.g. 2.0 and 3.5) in Team Build.  MSBuild added support for this sort of thing in the 3.5 framework (they call it multi-targeting), but it is not particularly easy to take advantage of it in Team Build.  Here's the text of my response:

MSBuild added a bunch of different mechanisms for specifying the ToolsVersion (i.e. Framework) to be used when building particular projects.  In particular:

  • You can specify the ToolsVersion at the command-line when invoking msbuild.exe using the /tv option.
  • You can specify the ToolsVersion when invoking the MSBuild task using the ToolsVersion property.
  • You can specify a default ToolsVersion using the ToolsVersion attribute on the <Project> element for a particular project.
  • You can specify a ToolsVersion metadata item when using the MSBuild task to build an item group containing one or more projects – see http://blogs.msdn.com/msbuild/archive/2007/07/17/toolsversion-metadata-for-items-used-in-msbuild-task-s.aspx.

None of these are particularly easy to use in combination with Team Build, unfortunately.  The /tv option will not work, since the Team Build traversal project (TfsBuild.proj + Microsoft.TeamFoundation.Build.targets) uses 3.5 syntax, and will not build using the 2.0 engine.  We don’t expose the ability to set ToolsVersion directly on the MSBuild task when we invoke your solution (we didn’t realize this would be useful, given the existence of the ToolsVersion attribute on the <Project> element).  And we build solutions by invoking the MSBuild task on a property containing the name of your solution, so you cannot use a ToolsVersion metadata item.

The simplest workaround here takes advantage of the properties made available by MSBuild in its generated *.sln.cache files (these are the msbuild projects created to build solutions – they used to be in-memory only, but are now typically saved to disk and reused).  In particular, something like the following should do the trick:

  <ItemGroup>
    <SolutionToBuild Include="SomeSolution.sln">
      <Properties>ProjectToolsVersion=3.5</Properties>
    </SolutionToBuild>
    <SolutionToBuild Include="SomeSolution.sln">
      <Properties>ProjectToolsVersion=2.0</Properties>
    </SolutionToBuild>
  </ItemGroup>

The idea is to build the solution twice, specifying the tools version to be used each time using the ProjectToolsVersion property exposed in the *.sln.cache file. 

There are all kinds of wacky properties exposed in the *.sln.cache files - I highly recommend taking a look at these if you're looking to do anything fancy with individual solutions in Team Build.  If you run msbuild 3.5 from the command-line on a solution, you will find a *.sln.cache file sitting alongside the *.sln file when you are done - open it up in VS (or notepad, if you're not into fancy formatting) and have a look!

In TFS 2008, TfsBuild.proj files can be located anywhere in source control, not just in $/TeamProject/TeamBuildTypes, as was required in TFS 2005.  As a result, we changed the default recursion type used to download files from the configuration folder path (the location of TfsBuild.proj) to RecursionType.OneLevel, meaning that only the contents of the exact directory of TfsBuild.proj are downloaded.  The rationale here is that if you for some reason decided to put your TfsBuild.proj file at $/TeamProject, a fully recursive download would get the entire contents of the repository for that team project, which is probably not what you would want!  In some cases, however, you may wish to do a fully recursive download (if you store custom task assemblies in a subdirectory under your configuration folder path, for example), so we did provide a backdoor for changing this behavior.  In particular, you can set the ConfigurationFolderRecursionType key in TfsBuildService.exe.config on your build machine to "Full" to get full recursion.  Something like:

<add key="ConfigurationFolderRecursionType" value="Full" />

...in the appSettings portion of the config file should do the trick.  Make sure to restart the service after making the change, and the next build should do a fully recursive download.

There are lots and lots of MSBuild properties available to Team Build 2008 build definitions, most of which are probably unknown to the majority of users.  As such, I've tried to compile a comprehensive list of these properties so that they can (hopefully) be more widely used.  I'll do the same thing at some point here for Team Build 2005.  If I've missed anything here or made any mistakes please let me know and I'll try and get them added/fixed.

Extensibility Properties

These are properties that are designed to be overridden by users, and they are already pretty well documented in MSDN here.  I've included the same properties here for comprehensiveness, along with some others not covered by the linked document:

  • AdditionalVCOverrides.  These values are written to the vsprops file generated by Team Build for each C++ project compiled and passed to the Override property of the VCBuild task.
  • BinariesSubdirectory.  The subdirectory under the build agent's working directory to which binaries are redirected during compilation.  Default value is "Binaries".
  • BuildConfigurationsInParallel.  Boolean property which, if true, enables building configurations (e.g. Debug|Any CPU and Release|Any CPU) in parallel, using MSBuild's multi-process capability.  Defaults to true.  Note that for this property to have an effect, the number of processes used by MSBuild, controlled by the MaxProcesses key in TfsBuildService.exe.config, must be set to a number greater than one.
  • BuildlogText.  The text that points to the build log in the work item created on a compilation failure.
  • BuildNumber.  The number of the build being built.  If you wish to override the default value, make sure to do so in the BuildNumberOverrideTarget so that the standard build process logic will update the various properties that depend on the build number (the drop location, label name, etc.) properly.
  • BuildSolutionsInParallel.  Boolean property which, if true, enables building solutions in parallel, using MSBuild's multi-process capability.  Defaults to true.  Note that for this property to have an effect, the number of processes used by MSBuild, controlled by the MaxProcesses key in TfsBuildService.exe.config, must be set to a number greater than one.
  • CleanCompilationOutputOnly.  Do not use this property directly - use IncrementaBuild and/or IncrementalGet instead.
  • CreateWorkspaceTaskComment.  The comment used when Team Build's workspace is created.  Default value is "Workspace created by Team Build".
  • CustomizableOutDir.  Set this property to true to keep Team Build from overriding the OutDir property for each solution/project in the SolutionToBuild item group. 
  • CustomizablePublishDir.  Set this property to true to keep Team Build from overriding the OutDir property for each solution/project in the SolutionToPublish item group.
  • CustomPropertiesForBuild.  Allows properties to be passed into all solutions/projects compiled during the course of the build.
  • CustomPropertiesForClean.  Allows properties to be passed into all solutions/projects cleaned during the course of the build.  Note that the Clean target of individual solutions/projects is only invoked if IncrementalBuild is true.
  • DescriptionText.  The history comment of the work item created on a compilation failure.
  • ErrorWarningLogText.  The text that points to the errors/warnings log file in the work item created on a compilation failure.
  • IncrementalBuild.  A boolean property that specifies whether the build should be full or incremental.  Defaults to false (i.e. a full build). 
  • IncrementalGet.  A boolean property that specifies whether the get operation should be full or incremental.  Defaults to false (i.e. a full get).
  • MSTestRefPath.  The path to the TestToolsTask (contained in Microsoft.VisualStudio.QualityTools.MSBuildTasks.dll).  Overridable so that the 8.0 version can be used for test assemblies compiled against the 8.0 unit test framework - see V8TestToolsTask.
  • RunCodeAnalysis.  Property that controls whether code analysis is executed according to project settings (Default), all the time (Always), or never (Never).
  • RunTest.  Boolean property that controls whether or not tests are executed.
  • SkipClean.  Do not use this property directly - use IncrementalGet and/or IncrementalBuild instead.
  • SkipDropBuild.  Set this property to true to skip the CoreDropBuild target.
  • SkipGet.  Set this property to true to skip the CoreGet target.
  • SkipGetChangesetsUpdateWorkItems.  Set this property to true to skip the association of changesets and work items for successful builds.
  • SkipInitializeWorkspace.  Do not use this property directly - use IncrementalGet and/or IncrementalBuild instead.
  • SkipInvalidConfigurations.  Set this property to false to generate an error rather than a warning when an invalid configuration is present in the ConfigurationToBuild item group for one or more solutions/projects.
  • SkipLabel.  Set this property to true to skip the CoreLabel target.
  • SkipPostBuild.  Do not use this property - use SkipGetChangesetsUpdateWorkItems instead.
  • SkipWorkItemCreation.  Set this property to true to skip the CoreCreateWorkItem target on a compilation failure.
  • SourcesSubdirectory.  The subdirectory under the build agent's working directory to which sources are retrieved from version control.  Default value is "Sources".
  • StopOnFirstFailure.  Set this property to true to stop cleaning, compiling, and/or testing on the first failure encountered.
  • TestResultsSubdirectory.  The subdirectory under the build agent's working directory to which test results are redirected.  Default value is "TestResults".
  • UpdateAssociatedWorkItems.  A boolean value that controls whether associated work items have their "Fixed In" field updated on a successful build.
  • UpdateAssociatedWorkItemsOnBuildBreak.  A boolean value that controls whether associated work items are updated for broken builds.  Default value is false.
  • V8TestToolsTask.  Boolean property which, if true, causes the 8.0 TestToolsTask syntax to be used (the TestNames property was not supported in the 8.0 version of the task).
  • VCBuildAdditionaLibPaths.  Passed to the AdditionalLibPaths property of the VCBuild task for each C++ project compiled.
  • VCBuildAdditionalOptions.  Passed to the AdditionalOptions property of the VCBuild task for each C++ project compiled.
  • VCBuildToolPath.  Passed to the ToolPath property of the VCBuild task for each C++ project compiled.
  • VCBuildUseEnvironment.  Passed to the UseEnvironment property of the VCBuild task for each C++ project compiled.
  • VCOverridesOpen.  The beginning of the vsprops file used to override VCBuild properties - can be overridden to use a different ProjectType version, a different Name, etc.  Default value is an escaped version of:

<?xml version="1.0"?>
<VisualStudioPropertySheet ProjectType="Visual C++" Version="8.00" Name="Team Build Overrides"

  • VCOverridesClose.  The ending of the vsprops file used to override VCBuild properties.  Default value is an escaped version of:

</VisualStudioPropertySheet>

  • WorkItemFieldValue.  The fields and values of the work item created on a compilation failure.
  • WorkItemTitle.  The title of the work item created on a compilation failure.
  • WorkItemType.  The type of work item created on a compilation failure.

Task Behavior Extensibility Properties

These are properties that are designed to be overridden by users to control the behavior of particular tasks (e.g. the Get task).  They are organized by task.

  • Get (for more info, see the tf get docs here)
    • ForceGet.  Passed to the Force property of the Get task.  Use of this property is discouraged - use the IncrementalGet and/or IncrementalBuild helper properties instead.
    • GetOverwrite.  Passed to the Overwrite property of the Get task.  If true (the default), writable files will be overwritten automatically.
    • RecursiveGet.  Passed to the Recursive property of the Get task.  If true (the default), the get operation will be recursive; otherwise, the get operation will only get top-level files and directories.
    • GetVersion.  Passed to the Version property of the Get task.  Defaults to SourceGetVersion (see below), but can be overridden to retrieve a particular version from source control.
    • GetFileSpec.  Passed to the Filespec property of the Get task.  Defaults to empty, signifying that the entire contents of the workspace should be retrieved.
    • GetPopulateOutput.  Passed to the PopulateOutput property of the Get task - controls whether the Gets, Replaces, and Deletes properties of the Get task are populated (these provide access to the new files retrieved from version control, the files modified during the get, and the files deleted during the get).  Default value is false.
  • Label (for more info, see the tf label docs here)
    • LabelRecursive.  Passed to the Recursive property of the Label task.  If true (the default), the label operation will be recursive; otherwise, the label operation will only label top-level files and directories.
    • LabelChild.  Passed to the Child property of the Label task.  Valid values are Merge or Replace, with the default being Replace
    • LabelComment.  Passed to the Comment property of the Label task, this property controls the comment text used by the label operation.  Default value is "Label created by Team Build".
    • LabelName.  Passed to the Name property of the Label task, this property controls the name of the label generated by the label operation.  Default value is the build number.
    • LabelFiles.  Passed to the Files property of the Label task, this property controls the files labeled by the label operation.  Default value is "$/", or every file mapped in the workspace.
    • LabelScope.  Passed to the Scope property of the Label task, this property controls the scope of the label generated by the label operation.  Default value is "$/$(TeamProject)".

Other Useful Properties

These are properties that are used by the build process and should not be modified.  They can, however, be used to execute logic conditionally, to parameterize custom logic in the build process, etc.

  • BinariesRoot.  The root directory for binaries - typically the working directory for the build agent + the BinariesSubdirectory property (see above).
  • BuildAgentName.  The name of the build agent running the build.
  • BuildAgentUri.  The URI of the build agent running the build.
  • BuildBreak.  Set to true when a compilation error occurs, otherwise false.  Can be used in build break targets to determine whether they are executing after a compilation error or executing normally.
  • BuildDefinition.  This property provides the name of the definition being built.
  • BuildDefinitionId.  This property provides the integer ID of the definition being built.
  • BuildDefinitionName.  See BuildDefinition.
  • BuildDefinitionUri.  The URI of the build definition being built.
  • BuildDirectory.  The local directory being used for the build, corresponding to the expanded working directory of the build agent being used.
  • BuildProjectFolderPath.  This property provides the version control path to the TfsBuild.proj file being run, and corresponds to the ConfigurationFolderPath property of the build definition.
  • BuildUri.  The URI of the executing build.
  • ConfigurationFolderUri.  The version control URI of the directory that contains the TfsBuild.proj file being run.
  • DropLocation.  This property provides the "root" drop location for the build - either the DefaultDropLocation of the build definition or the overridden drop location provided when the build was queued.
  • IsDesktopBuild.  True for desktop builds, false for "standard" build machine builds.
  • LastBuildNumber.  The number of the last build, regardless of its status.
  • LastGoodBuildLabel.  The label created by the last "good" build.  This is the label that will be used to associate changesets and work items with the current build.
  • LastGoodBuildNumber.  The number of the last good build.
  • MachineName.  The machine name of the build agent running the build.
  • MaxProcesses.  The maximum number of processes being used by MSBuild.
  • Port.  The port being used by the build agent running the build.
  • NoCICheckinComment.  The comment which must be included in a checkin to prevent continuous integration from triggering a build.  This is critical for CI builds that need to check files in to avoid triggering an infinite loop of builds.
  • Reason.  (SP1 and later only)  The reason the build was started - None indicates the build was manually queued, Individual indicates that a "build every checkin" trigger caused the build, Batch indicates that a "rolling build" trigger caused the build, Schedule indicates that a scheduled trigger caused the build, and ScheduleForced indicates that a scheduled trigger that builds whether or not changes have occurred since the last build caused the build.
  • RequestedBy.  The user that requested the build - for triggered builds, this will typically be a service account.
  • RequestedFor.  The user for whom the build was requested - for CI builds, this will be the user whose checkin triggered the build.
  • SolutionRoot.  The root directory for source files retrieved from version control - typically the working directory for the build agent + the SourcesSubdirectory property (see above).
  • SourceGetVersion.  The version spec that will be used to retrieve sources for the build, unless the GetVersion property is overridden.
  • StartTime.  The time at which the build started.
  • TeamBuildConstants.  Allows projects being compiled to determine whether Team Build is compiling them.  Default value is "_TEAM_BUILD_".
  • TeamBuildOutDir.  The directory Team Build would have set OutDir true if CustomizableOutDir and/or CustomizablePublishDir (see above) were false.  If both are true, this value is equivalent to OutDir
  • TeamBuildRefPath.  Provides the path to Team Build binaries (the logger, tasks, etc.).  Typically %ProgramFiles%\Microsoft Visual Studio 9.0\Common7\IDE\PrivateAssemblies.
  • TeamBuildVersion.  This property is set to 2.0 in Team Build 2008 and will be incremented in subsequent versions.
  • TeamFoundationServerUrl.  The URL of the team foundation server that owns the executing build.
  • TeamProject.  The team project of the build definition being built.
  • TestResultsRoot.  The root directory for test results - typically the working directory for the build agent + the TestResultsSubdirectory property (see above).
  • WorkspaceName.  The name of the workspace used to retrieve sources from version control. 

Target Dependency Properties

These are the properties used to set up the target dependency hierarchy in Team Build.  The can be modified to insert custom targets into the build process as an alternative to overriding the provided extensibility targets, but this should be done with care.  The standard pattern for overriding a dependency property is something like the following:

  <PropertyGroup>
    <DesktopBuildDependsOn>
      PreDesktopBuildTarget;
      $(DesktopBuildDependsOn);
      PostDesktopBuildTarget;
    </DesktopBuildDependsOn>    
  </PropertyGroup>

Note that the initial value of the property is preserved and new values are simply inserted before and after the existing ones, making sure to include semi-colons in all the appropriate spots.  For more information on these properties, you'll need to comb through the targets file - a full explanation of their functionality is beyond the scope of this blog post!

  • CleanDependsOn.
  • CleanAllDependsOn.
  • CleanCompilationOutputDependsOn.
  • CleanConfigurationDependsOn.
  • CleanSolutionDependsOn.
  • CompileDependsOn.
  • CompileConfigurationDependsOn.
  • CompileSolutionDependsOn.
  • ComputeSolutionListDependsOn.
  • CoreCompileDependsOn.  (This one is for backwards compatibility with TFS 2005 target overrides)
  • _CoreCompileDependsOn.  (This one is the new CoreCompileDependsOn for TFS 2008)
  • CoreCompileConfigurationDependsOn.
  • CoreCleanDependsOn.
  • CoreCleanAllDependsOn.
  • CoreCleanCompilationOutputDependsOn.
  • CoreCleanConfigurationDependsOn.
  • CoreCreateWorkItemDependsOn.
  • CoreDropBuildDependsOn.
  • CoreGetChangesetsAndUpdateWorkItemsDependsOn.
  • CoreGetChangesetsOnBuildBreakDependsOn.
  • CoreGetDependsOn.
  • CoreInitializeWorkspaceDependsOn.
  • CoreLabelDependsOn.
  • CoreOnBuildBreakDependsOn.
  • CoreTestDependsOn.
  • CoreTestConfigurationDependsOn.
  • CreateWorkItemDependsOn.
  • DesktopBuildDependsOn
  • DesktopRebuildDependsOn.
  • DropBuildDependsOn.
  • EndToEndIterationDependsOn.
  • GetChangesetsAndUpdateWorkItemsDependsOn.
  • GetChangesetsOnBuildBreakDependsOn.
  • GetDependsOn.
  • InitializeWorkspaceDependsOn.
  • LabelDependsOn.
  • OnBuildBreakDependsOn.
  • PostBuildDependsOn.
  • PreBuildDependsOn.
  • RunTestDependsOn.
  • TeamBuildDependsOn.
  • TestConfigurationDependsOn.
  • TestDependsOn.
More Posts Next page »
 
Page view tracker