Introduction
In Team Build, we target to cover the complete code base of our product with continuous builds, i.e., make an attempt for continuous integration. In such cases, builds are triggered as soon as check-in happens and the stake-holders are informed about the build failure, success and other useful information. That’s really cool. Let us take a step towards the real world. It’s not useful or practical to integrate the complete code base within one build, simply because it may take several hours to build the complete code and will give the information only at a higher level, e.g. - The build breaks and the reason could be any one of the several 50 associated change-sets which happened since the last build. This is why we tend to create more number of continuous builds and try to club small solutions together so that an average build takes 2 minutes to 20 minutes and we get the information at a lower level directly pointing out the developer and the change-set that broke the build. So, understanding the fact that we need several smaller builds to cover the complete code base by logically grouping them, we must agree that at the same time we need to have a build which integrates several continuous builds and executes them together as a group so that we can build some solutions together and check for integration errors and many more (probably on a single dedicated build agent)!!! Let’s call the integrations build as hourly build, assuming it will try to integrate a couple of smaller continuous builds every hour.
Having said that, I can write my continuous build definitions code in the .targets file, instead of writing the MSBuild script in the TFSBuild.proj, so that it can be reused in the hourly build, but here comes the problem of workspace mappings. In order for the hourly build to work, it needs all the workspace mappings of all the continuous builds it’s going to execute and believe me, it’s no point adding them manually and maintaining them. The workspace mappings of the continuous builds itself keep on changing very rapidly and there will be lots of conflicts while considering the mappings of all the continuous builds together in a single build. So we need some way to get this workspace mapping automatically added in the hourly builds as soon as they are executed. This article talks about implementing a Workspace Template Manager and plugging it in the correct event of the build cycle, such that it performs the expected task and team build accepts the changes done in the same build definition which it has already started processing!
Background
I faced this challenge in the middle month of the third quarter of 2009. At first, it looked to me as a solvable problem, but I could not make it work. I tried all approaches like using the vast API of TFS, MSBuild scripts, tf.exe command like utility and Googling of course. Then my development lead came in and we tried together for some days. Afterwards, fortunately, again I took some more time and with good luck, this is the solution I implemented and it worked without any complaints. All those working on continuous / hourly kind of framework for their builds must have faced this problem. There is no solution present yet! So, here you go for the solution.
Visualize the Challenge
The above image on the left, shows a typical product source control structure, having lots of code like multiple solutions, projects for Business Logic and Data Access Layer Code, etc. On the right, it shows the continuous builds targeting these solutions separately. This is what we just discussed above, that there are several continuous builds for covering the smaller parts of source code independently. In the middle is shown the workspace mappings of one of the continuous builds. Similarly, we have a different workspace template for every build. Here, we need to have one hourly build definition 'Hourly.All_BL_With_DAL
' which builds all the solutions of Business Logic and DAL and hence needs to have all the workspace mappings present in all of these builds, as shown below, regardless of the frequency other builds are changed and the expected conflicts across build definitions.
Implementing the Solution
These workspace mappings are a difficult challenge to play with. As soon as a build is queued, among the starting steps team build internally performs lot of tasks like assigning workspace name, its initialization, mappings, etc. So manipulating the workspace gives lots of run time errors and in cases even no error and no results as well. These mappings can be played in only one event in the build cycle and only with the below discussed API, it's then that the changes are accepted by the team build and processed immediately within the current build. The below approach will process the workspace mappings in the build as soon as it starts getting processed, i.e., it will execute the hourly build with the latest workspace mappings required and that is what we wanted.
We will implement the solution by writing our logic in a custom task to be called by the MSBuild script in the correct event ‘BeforeInitializeWorkspace
’ of the build cycle.
The below custom task – WorkspaceTemplateManager
, can be built and placed as a .dll in the TeamBuild folder of the build agent, so that it can be referred from the MSBuild scripts. The code is very well explained with the inline comments. This task has an overridden Execute
method which performs these steps:
- Find out all the continuous builds script files referred from this hourly build.
- Get the build definitions object model instance using tfs API.
- Initiate the process of getting the workspace details from all the continuous builds and updating them in the hourly builds and take care of all conflicts. This can be better read from the below code inline / block comments.
using System;
using System.IO;
using System.Xml;
using Microsoft.Build.Utilities;
using Microsoft.Build.Framework;
using System.Collections.Generic;
using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.Build.Client;
using Microsoft.TeamFoundation.VersionControl.Client;
namespace RA.TeamFoundation.Build.Tasks
{
public class WorkspaceTemplateManager : Task
{
public string TeamFoundationServerUrl { get; set; }
public string TeamProject { get; set; }
public string BuildDefinitionName { get; set; }
public string BuildProjectFilePath { get; set; }
private bool _sucessfullyExecuted;
public override bool Execute()
{
try
{
List<string> continuousTargetsFiles = GetAllContinuousTargetsFiles();
List<IBuildDefinition> contBuildDefinitions =
RetriveBuildDefinitions(continuousTargetsFiles);
IdentifyAndUpdateWorkspaceTemplate(contBuildDefinitions);
_sucessfullyExecuted = true;
}
catch
{
_sucessfullyExecuted = false;
}
return _sucessfullyExecuted;
}
private void IdentifyAndUpdateWorkspaceTemplate(List<IBuildDefinition>
selectedContinuousBuildDefinitions)
{
IBuildDefinition hourlyBuildDefintion = GetBuildDefinitionByName
(TeamFoundationServerUrl, TeamProject, BuildDefinitionName);
hourlyBuildDefintion.Refresh();
IWorkspaceTemplate currnetWorkspaceTemplate = hourlyBuildDefintion.Workspace;
foreach (IBuildDefinition selectedContBuildDefinition
in selectedContinuousBuildDefinitions)
{
foreach (IWorkspaceMapping continuousMappingToAdd
in selectedContBuildDefinition.Workspace.Mappings)
{
bool blnSafeToAddNewMapping = true;
foreach (IWorkspaceMapping currentMapping in
currnetWorkspaceTemplate.Mappings)
{
if (currentMapping.ServerItem.Equals
(continuousMappingToAdd.ServerItem))
{
if ((currentMapping.MappingType ==
WorkspaceMappingType.Cloak)
&& (continuousMappingToAdd.MappingType ==
WorkspaceMappingType.Map))
{
currnetWorkspaceTemplate.RemoveMapping(currentMapping);
blnSafeToAddNewMapping = true;
}
else
{
if (!String.IsNullOrEmpty(currentMapping.LocalItem)
&& !String.IsNullOrEmpty
(continuousMappingToAdd.LocalItem)
&& !currentMapping.LocalItem.Equals
(continuousMappingToAdd.LocalItem))
{
currnetWorkspaceTemplate.RemoveMapping
(currentMapping);
blnSafeToAddNewMapping = true;
}
else
{
blnSafeToAddNewMapping = false;
}
}
break;
}
}
if (blnSafeToAddNewMapping)
currnetWorkspaceTemplate.AddMapping
(continuousMappingToAdd.ServerItem,
continuousMappingToAdd.LocalItem,
continuousMappingToAdd.MappingType,
continuousMappingToAdd.Depth);
}
}
hourlyBuildDefintion.Save();
hourlyBuildDefintion.Refresh();
}
private List<IBuildDefinition> RetriveBuildDefinitions(List<string> targetsFiles)
{
TeamFoundationServer tfs = new TeamFoundationServer(TeamFoundationServerUrl);
IBuildServer buildServer =
tfs.GetService(typeof(IBuildServer)) as IBuildServer;
VersionControlServer vcs =
tfs.GetService(typeof(VersionControlServer)) as VersionControlServer;
IBuildDefinition[] allBuildDefinitions =
buildServer.QueryBuildDefinitions(TeamProject);
List<IBuildDefinition> selectedBuildDefinitions =
new List<IBuildDefinition>();
foreach (string targetsFile in targetsFiles)
{
foreach (IBuildDefinition buildDefinition in allBuildDefinitions)
{
string path = buildDefinition.ConfigurationFolderPath +
"/" + targetsFile;
if (!vcs.ServerItemExists(path, ItemType.File))
continue;
selectedBuildDefinitions.Add(buildDefinition);
}
}
return selectedBuildDefinitions;
}
private static IBuildDefinition GetBuildDefinitionByName
(string teamFoundationServerUrl, string teamProjectName,
string buildDefinitionName)
{
TeamFoundationServer tfs = new TeamFoundationServer(teamFoundationServerUrl);
IBuildServer buildServer = tfs.GetService
(typeof(IBuildServer)) as IBuildServer;
IBuildDefinition buildDefinition =
buildServer.GetBuildDefinition(teamProjectName, buildDefinitionName);
return buildDefinition;
}
private List<string> GetAllContinuousTargetsFiles()
{
XmlNodeList importedProjects = GetImportedProjects();
List<string> targetsFiles = new List<string>();
foreach (XmlNode importedProject in importedProjects)
{
string projFileName = GetProjectFileName(importedProject);
targetsFiles.Add(projFileName);
}
return targetsFiles;
}
private XmlNodeList GetImportedProjects()
{
XmlDocument projectFile = new XmlDocument();
projectFile.Load(BuildProjectFilePath);
XmlNamespaceManager nsmngr = new XmlNamespaceManager(projectFile.NameTable);
nsmngr.AddNamespace("pr",
"http://schemas.microsoft.com/developer/msbuild/2003");
XmlNodeList importNodes = projectFile.SelectNodes
("pr:Project/pr:Import", nsmngr);
return importNodes;
}
private static string GetProjectFileName(XmlNode importedProject)
{
XmlAttribute projectAttriubute = importedProject.Attributes["Project"];
string targetsFile = Path.GetFileName(projectAttriubute.Value);
return targetsFile;
}
}
}
Then we need to invoke this custom task from the hourly build. This is done by invoking it from the BeforeInitializeWorkspace
target and passing the required parameters. Below is the sample hourly build targets file to show this invocation of task and inclusion of continuous builds targets file in it. This also has been decorated with inline comments.
="1.0"="utf-8"
<Project DefaultTargets="DesktopBuild"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="3.5">
<PropertyGroup>
<TargetsLocalRoot>$(MSBuildExtensionsPath)\Microsoft\VisualStudio\TeamBuild\Main
</TargetsLocalRoot>
</PropertyGroup>
<UsingTask
TaskName="RA.TeamFoundation.Build.Tasks.WorkspaceTemplateManager"
AssemblyFile="$(TargetsLocalRoot)\RA.TeamFoundation.Build.Tasks.dll"/>
<Target Name="BeforeInitializeWorkspace">
<WorkspaceTemplateManager
TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
TeamProject="$(TeamProject)"
BuildDefinitionName="$(BuildDefinition)"
BuildProjectFilePath="$(TargetsLocalRoot)\Hourly.All_BL_With_DAL.targets" />
</Target>
<Import Project="$(TargetsLocalRoot)\Continuous.BL_Controllers.targets" />
<Import Project="$(TargetsLocalRoot)\Continuous.BL_CoreLogic.targets" />
<Import Project="$(TargetsLocalRoot)\Continuous.BL_WebServices.targets" />
<Import Project="$(TargetsLocalRoot)\Continuous.DAL_DataAccessCode.targets" />
</Project>
Conclusion
So, using the above solution will actually give the latest workspace mappings to the hourly builds, every time before they are processed, and will take care of any latest change in the continuous builds and will reflect it immediately in the hourly build.