工作区模板管理器,用于智能更新构建的工作区信息






4.85/5 (26投票s)
工作区模板管理器,
引言
在 Team Build 中,我们的目标是用持续生成覆盖产品的整个代码库,即尝试持续集成。在这种情况下,一旦发生签入,就会触发生成,并通知利益相关者生成失败、成功和其他有用信息。这真的很棒。让我们向现实世界迈出一步。将整个代码库集成到一个生成中是无益或不切实际的,因为构建整个代码可能需要几个小时,并且只能在更高层次上提供信息,例如 - 生成中断,原因可能是自上次生成以来发生的几十个相关更改集中的任何一个。这就是为什么我们倾向于创建更多数量的持续生成,并尝试将小的解决方案组合在一起,以便平均生成需要 2 分钟到 20 分钟,我们可以在较低级别获得直接指向开发人员和导致生成中断的更改集的信息。因此,理解我们需要多个小型生成来通过逻辑分组来覆盖整个代码库,我们必须同意,同时我们需要一个生成来集成多个持续生成并将它们作为一个组一起执行,以便我们可以一起构建一些解决方案并检查集成错误以及更多(可能在一个专用的生成代理上)!我们将集成生成称为每小时生成,假设它将尝试每小时集成几个小型持续生成。
话虽如此,我可以在 .targets 文件中编写我的持续生成定义代码,而不是在 TFSBuild.proj 中编写 MSBuild 脚本,以便它可以在每小时生成中重用,但这时就会出现工作区映射的问题。为了使每小时生成正常工作,它需要它将要执行的所有持续生成的全部工作区映射,相信我,手动添加并维护它们毫无意义。持续生成本身的工作区映射变化非常快,并且在考虑单个生成中所有持续生成的映射时会出现大量冲突。因此,我们需要一种方法能够在每小时生成执行时自动将其中的工作区映射添加进去。本文讨论了实现工作区模板管理器并将其插入生成周期的正确事件,以便它执行预期的任务,并且 Team Build 接受在它已开始处理的同一生成定义中所做的更改!
背景
我在 2009 年第三季度的中间月份面临了这个挑战。起初,我觉得这是一个可以解决的问题,但我无法让它正常工作。我尝试了所有方法,比如使用 TFS 的庞大 API、MSBuild 脚本、tf.exe 命令行实用程序以及当然还有谷歌搜索。然后我的开发主管过来了,我们一起尝试了几天。之后,幸运的是,我又花了一些时间,并且好运,这是我实现的解决方案,它工作得很好,没有任何抱怨。所有从事持续/每小时生成框架工作的人都可能面临这个问题。目前还没有解决方案!所以,这是解决方案。
可视化挑战
上面的图片左侧显示了一个典型的产品源代码结构,包含大量代码,如多个解决方案、业务逻辑和数据访问层代码的项目等。右侧显示了针对这些解决方案的持续生成。这正是我们刚才讨论的,有几个持续生成用于独立覆盖源代码的小部分。中间显示了一个持续生成的 istance 的工作区映射。类似地,我们为每个生成都有一个不同的工作区模板。在这里,我们需要一个每小时生成定义 'Hourly.All_BL_With_DAL
',它生成业务逻辑和 DAL 的所有解决方案,因此需要所有这些生成中的所有工作区映射,如下所示,无论其他生成更改的频率以及构建定义之间的预期冲突。
实现解决方案
这些工作区映射很难处理。一旦生成被排队,在开始的步骤中,Team Build 内部会执行大量任务,例如分配工作区名称、初始化、映射等。因此,操作工作区会导致很多运行时错误,在某些情况下甚至不会产生任何错误和结果。这些映射只能在生成周期的一个事件中进行操作,并且只能使用下面讨论的 API,此时 Team Build 接受更改并立即在当前生成中处理。下面的方法将在生成开始处理工作区映射时处理它们,即,它将使用最新的工作区映射执行每小时生成,而这正是我们想要的。
我们将通过将逻辑写入一个自定义任务来实现解决方案,该任务将由 MSBuild 脚本在生成周期的正确事件 'BeforeInitializeWorkspace
' 中调用。
下面的自定义任务 – WorkspaceTemplateManager
,可以构建并放置为 .dll 文件在生成代理的 TeamBuild 文件夹中,以便可以从 MSBuild 脚本中引用它。代码通过内联注释进行了很好的解释。此任务有一个重写的 Execute
方法,它执行以下步骤
- 找出此每小时生成引用的所有持续生成脚本文件。
- 使用 tfs API 获取生成定义对象模型实例。
- 启动从所有持续生成获取工作区详细信息并将其更新到每小时生成的过程,并处理所有冲突。这可以更好地从下面的代码内联/块注释中阅读。
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
{
// The team foundation server to run tasks against.
public string TeamFoundationServerUrl { get; set; }
// The name of the team project to get build definitions from.
public string TeamProject { get; set; }
// Current Build definition name, to be able to access its
// run time object using API
public string BuildDefinitionName { get; set; }
// Current Build proj file i.e. a .targets file or a
// .proj file to read the msbuild scripts at run time
public string BuildProjectFilePath { get; set; }
// A bool to update the msbuild scripts whether this task successfully executed
// or otherwise, if a build break is required.
private bool _sucessfullyExecuted;
public override bool Execute()
{
try
{
// get the list of all .targets files imported
// in the current build definition script.
// This will be used to find out all the continuous builds
// attached to this hourly build
List<string> continuousTargetsFiles = GetAllContinuousTargetsFiles();
// get the list of all other build definitions
// attached to the current build
List<IBuildDefinition> contBuildDefinitions =
RetriveBuildDefinitions(continuousTargetsFiles);
// Initiate the process of identifying the workspace mappings "correctly"
// from all the continuous builds and update them
// in the current hourly build
IdentifyAndUpdateWorkspaceTemplate(contBuildDefinitions);
_sucessfullyExecuted = true;
}
catch
{
// Any exception will actually fail the task
_sucessfullyExecuted = false;
}
return _sucessfullyExecuted;
}
//
// This function iterates through all the continuous build
// definition's workspace mappings
// and finds out if this mapping can be directly added / updated
// or there are conflicts with other mapping types and resolves them
// It also sees the major possibility of skipping the edit part,
// as it's already present
//
private void IdentifyAndUpdateWorkspaceTemplate(List<IBuildDefinition>
selectedContinuousBuildDefinitions)
{
// get the current hourly build and its workspace template
IBuildDefinition hourlyBuildDefintion = GetBuildDefinitionByName
(TeamFoundationServerUrl, TeamProject, BuildDefinitionName);
hourlyBuildDefintion.Refresh();
IWorkspaceTemplate currnetWorkspaceTemplate = hourlyBuildDefintion.Workspace;
// iterate through all the continuous build definition to see
// the possibility of their mappings to add / edit in the hourly build
foreach (IBuildDefinition selectedContBuildDefinition
in selectedContinuousBuildDefinitions)
{
// iterate through each and every mapping of the continuous build
foreach (IWorkspaceMapping continuousMappingToAdd
in selectedContBuildDefinition.Workspace.Mappings)
{
// assume it's safe to directly add this mapping
// to the parent hourly build
bool blnSafeToAddNewMapping = true;
// iterate through all the existing mappings of the hourly workspace
// to find out any possibility of conflict and
// remove / skip / edit that mapping accordingly
foreach (IWorkspaceMapping currentMapping in
currnetWorkspaceTemplate.Mappings)
{
// if the mapping to be added is already present,
// then we need to resolve the conflict
if (currentMapping.ServerItem.Equals
(continuousMappingToAdd.ServerItem))
{
// There are 4 combinations possible here
// for conflicts between already added mappings
// Case No: Existing Mapping Type ->
// New Mapping Type => Action to be taken
// Case 1: Cloak - Cloak => No Action required
// Case 2: Cloak - Map =>
// Remove the existing mapping and read the new mapping
// Case 3: Map - Map => Consider the local items.
// If local items are not same,
// remove the complete mapping and read it again,
// else no action required.
// Case 4: Map - Cloak => No action required.
// This is to cover all cases for conflicts between
// multiple build definitions
if ((currentMapping.MappingType ==
WorkspaceMappingType.Cloak)
&& (continuousMappingToAdd.MappingType ==
WorkspaceMappingType.Map))
{
// Case 2:
currnetWorkspaceTemplate.RemoveMapping(currentMapping);
blnSafeToAddNewMapping = true;
}
else
{
// Case 3:
if (!String.IsNullOrEmpty(currentMapping.LocalItem)
&& !String.IsNullOrEmpty
(continuousMappingToAdd.LocalItem)
&& !currentMapping.LocalItem.Equals
(continuousMappingToAdd.LocalItem))
{
currnetWorkspaceTemplate.RemoveMapping
(currentMapping);
blnSafeToAddNewMapping = true;
}
else
{
// Case 1: and Case 4:
blnSafeToAddNewMapping = false;
}
}
// safely break the loop, as the server
// item string is always unique in the workspace.
// (This conforms the workspace concepts as well)
break;
}
}
// Reading the mapping if was not present or for Case 2 or
// for Case 3
// add the new safe workspace mapping to the hourly build,
// with the same local item, mapping type and depth as well
if (blnSafeToAddNewMapping)
currnetWorkspaceTemplate.AddMapping
(continuousMappingToAdd.ServerItem,
continuousMappingToAdd.LocalItem,
continuousMappingToAdd.MappingType,
continuousMappingToAdd.Depth);
}
}
// save the hourly build definition
hourlyBuildDefintion.Save();
hourlyBuildDefintion.Refresh();
}
//
// Gets a list of build definition names from the .targets files.
//
private List<IBuildDefinition> RetriveBuildDefinitions(List<string> targetsFiles)
{
// connect to the tfs server, build server and version control server
TeamFoundationServer tfs = new TeamFoundationServer(TeamFoundationServerUrl);
IBuildServer buildServer =
tfs.GetService(typeof(IBuildServer)) as IBuildServer;
VersionControlServer vcs =
tfs.GetService(typeof(VersionControlServer)) as VersionControlServer;
// query all the build definitions of the team project
IBuildDefinition[] allBuildDefinitions =
buildServer.QueryBuildDefinitions(TeamProject);
// select list of continuous build definitions we are interested in
List<IBuildDefinition> selectedBuildDefinitions =
new List<IBuildDefinition>();
// iterate through all the target files and all the
// build definitions and find out the
// builds configured with these target files.
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;
}
//
// This function returns the object model
// build definitions instance from its name
//
private static IBuildDefinition GetBuildDefinitionByName
(string teamFoundationServerUrl, string teamProjectName,
string buildDefinitionName)
{
// connect to tfs and build server
TeamFoundationServer tfs = new TeamFoundationServer(teamFoundationServerUrl);
IBuildServer buildServer = tfs.GetService
(typeof(IBuildServer)) as IBuildServer;
// retrieve an instance of the build definition from its name
IBuildDefinition buildDefinition =
buildServer.GetBuildDefinition(teamProjectName, buildDefinitionName);
return buildDefinition;
}
//
// This function returns the list of all proj / .target files
// imported by the current build definition
// i.e. the list of all other continuous builds which will be
// executed as part of this build.
//
private List<string> GetAllContinuousTargetsFiles()
{
// get the list of imported project nodes
XmlNodeList importedProjects = GetImportedProjects();
List<string> targetsFiles = new List<string>();
foreach (XmlNode importedProject in importedProjects)
{
// get the list of imported project file names
string projFileName = GetProjectFileName(importedProject);
targetsFiles.Add(projFileName);
}
return targetsFiles;
}
//
// This function returns the list of all projects to be
// executed by the current build
//
private XmlNodeList GetImportedProjects()
{
// Load the current build script XML file to process its XML nodes.
XmlDocument projectFile = new XmlDocument();
projectFile.Load(BuildProjectFilePath);
XmlNamespaceManager nsmngr = new XmlNamespaceManager(projectFile.NameTable);
nsmngr.AddNamespace("pr",
"http://schemas.microsoft.com/developer/msbuild/2003");
// get the project/import nodes
// i.e. all the .proj or .targets files imported.
XmlNodeList importNodes = projectFile.SelectNodes
("pr:Project/pr:Import", nsmngr);
return importNodes;
}
//
// This function returns the imported project file name
//
private static string GetProjectFileName(XmlNode importedProject)
{
XmlAttribute projectAttriubute = importedProject.Attributes["Project"];
string targetsFile = Path.GetFileName(projectAttriubute.Value);
return targetsFile;
}
}
}
然后我们需要从每小时生成中调用此自定义任务。这是通过在 BeforeInitializeWorkspace
目标中调用它并传递必需的参数来完成的。下面是演示此任务调用和包含持续生成目标文件的示例每小时生成目标文件。这也已用内联注释进行了装饰。
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="DesktopBuild"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="3.5">
<!-- The location where all the .targets files of the product are kept-->
<PropertyGroup>
<TargetsLocalRoot>$(MSBuildExtensionsPath)\Microsoft\VisualStudio\TeamBuild\Main
</TargetsLocalRoot>
</PropertyGroup>
<!-- Declare the custom task name and the DLL name which will be
called from the build events / targets -->
<UsingTask
TaskName="RA.TeamFoundation.Build.Tasks.WorkspaceTemplateManager"
AssemblyFile="$(TargetsLocalRoot)\RA.TeamFoundation.Build.Tasks.dll"/>
<!-- This is the only event in the build cycle where the workspace
template manager task can be called and executed
and changes will be accepted and immediately processed by the
team build within the currently executed build -->
<Target Name="BeforeInitializeWorkspace">
<WorkspaceTemplateManager
TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
TeamProject="$(TeamProject)"
BuildDefinitionName="$(BuildDefinition)"
BuildProjectFilePath="$(TargetsLocalRoot)\Hourly.All_BL_With_DAL.targets" />
</Target>
<!-- The below shows that this hourly build definition actually will
process all the below 4 continuous builds -->
<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>
结论
因此,使用上述解决方案实际上将为每小时生成提供最新的工作区映射,每次在处理它们之前,并处理持续生成中的任何最新更改,并立即在每小时生成中反映出来。