针对(未记录的)发布管理 API 进行编程
如何导航和编程使用发布管理 API,即使它没有官方文档。
引言
本文的主要目的是让您能够使用发布管理 API,甚至可以执行本文中未明确讨论的操作。它可能是未记录的,但并非无法发现。
据推测,当文档可用时,它将位于 https://www.visualstudio.com/en-us/integrate/api/overview。还有一个 UserVoice 条目要求 Microsoft 提供官方文档。
我将演示的示例是如何创建一个草稿模式下的发布,编辑该特定发布的自定义配置变量的值,然后启动该发布。
当我研究这个主题时,我找到的示例只解释了如何
- 使用 Powershell 触发发布而不修改它,使用 WebAPI
- 使用 C# 执行各种操作(包括触发发布),但其方式似乎很难发现如何使用示例中未明确包含的功能。
要求
要使用发布管理 API,您需要访问发布管理客户端及其安装目录。
要能够自行导航 API 以找出未在本文中明确提及的操作方法,您需要安装 Fiddler (它是免费的)。
特定示例的背景
本文中的示例使用了一个 vNext 发布模板,其中包含 2 个组件(MyComponent1 和 MyComponent2)。两个组件都设置为“通过应用程序构建”。
开箱即用地,使用相同组件和相同计算机的 vNext 模板不能同时用于发布,因为它们会尝试将文件放置在完全相同的目录中。
我需要在同一台机器上为所有应用程序运行部署的一部分,并且我不想为每个阶段的每个应用程序创建一个单独的组件。
发布管理提供了几个定义变量的位置,并且它有一个名为 ApplicationPathRoot 的系统变量,您可以覆盖该变量以更改复制组件的目标路径。详细信息位于 https://msdn.microsoft.com/en-us/library/dn834972(v=vs.120).aspx。
覆盖每个发布的应用路径根值(ApplicationPathRoot value)的唯一位置是为模板中的每个操作添加一个名为 ApplicationPathRoot 的自定义配置变量。
此示例适用于 RM 2015,但这些概念应适用于任何版本。
设置解决方案
首先,我们需要创建一个新的 C# 项目。一个简单的控制台应用程序足以满足此示例的要求。
我们将需要引用的实际 API Dll 位于发布管理客户端安装的应用程序目录中(对我来说,它是 C:\Program Files (x86)\Microsoft Visual Studio 14.0\Release Management\Client\bin)。我们需要从该文件夹中引用的 Dll 是
- Microsoft.TeamFoundation.Release.Common
- Microsoft.TeamFoundation.Release.CommonResources
- Microsoft.TeamFoundation.Release.Data
- Microsoft.TeamFoundation.Release.Data2
- Microsoft.TeamFoundation.Release.Data3
- Microsoft.Practices.EnterpriseLibrary.Validation
我们还需要添加一个引用到
- WindowsBase.dll。
我们需要将我们的控制台应用程序配置为指向我们的发布管理服务器。为此,请将 Microsoft.TeamFoundation.Release.Data.dll.config(来自 RM 客户端应用程序文件夹)的内容复制到我们控制台应用程序的 App.config 中。注意:“supportedRuntime”部分不应复制到我们的 App.config 中,否则应用程序可能无法正常启动。
弄清楚从哪里开始
现在我们已经设置好了解决方案,我们如何知道需要在代码中引用哪个类型?您可以直接跳转到下面的代码示例,但如果您想学习如何自己弄清楚,请打开 Fiddler。
Fiddler 允许我们查看与您的计算机之间传输的所有流量。由于发布管理客户端通过 WebAPI 与服务器通信,我们可以手动在客户端执行一些操作,Fiddler 会告诉我们等效的 WebAPI 是什么,这与 .NET API 相当接近。
所以,对于我们的示例,在客户端创建一个草稿模式的发布。Fiddler 会显示几个(对我来说是 19 个)请求(在 RM 中单击任何内容后,有多个请求加载下一个屏幕)。我们需要翻阅这些请求,查看请求标头(Request Header)以找到听起来像我们想要的东西。以下是我在创建发布草稿后看到的一些请求标头:
-
POST /account/releaseManagementService/_apis/releaseManagement/ReleaseV2Service/ListReleases?api-version=6.0 HTTP/1.1
-
POST /account/releaseManagementService/_apis/releaseManagement/ConfigurationService/GetApplicationVersion?api-version=6.0 HTTP/1.1
-
POST /account/releaseManagementService/_apis/releaseManagement/OrchestratorService/CreateRelease?releaseTemplateName=MyReleaseTemplate&deploymentPropertyBag=%7B%22ReleaseName%22%3A%22MyReleaseName%22%2C%22ReleaseBuild%22%3A%22MyCompany.MyApplication_1.0.1%22%2C%22ReleaseBuildChangeset%22%3A%22%22%2C%22TargetStageId%22%3A%223%22%2C%22MyComponent1%3ABuild%22%3A%22%22%2C%22MyComponent1%3ABuildChangesetRange%22%3A%22-1%2C-1%22%2C%22MyComponent2%3ABuild%22%3A%22%22%2C%22MyComponent2%3ABuildChangesetRange%22%3A%22-1%2C-1%22%7D&api-version=6.0 HTTP/1.1
首先,请注意这些请求的布局非常相似:
/account/releaseManagementService/_apis/releaseManagement/{ServiceName}/{MethodName}?{Parameters}。
CreateRelease 方法听起来是我们想要的,提供该方法的服务是 OrchestratorService。
我(通过查看反编译的 RM Dll)发现,如果我们想要一个 ServiceName 实例,我们需要通过以下两种方式之一获取 IServiceName 对象(取决于需要哪个服务):
- {ServiceName}Factory.Instance
- Services.{ServiceName}
所以,在这种情况下,我们需要:
IOrchestratorService orchestratorService = OrchestratorServiceFactory.Instance;
编写代码
现在我们有了实例,我们可以使用智能感知(intellisense)找到它有一个 CreateRelease 方法,该方法接受与我们上面看到的相同的参数!第一个参数是 releaseTemplateName
,这显然是我们希望用于创建发布的发布的发布模板的名称。
第二个参数就没那么明显了。它是一个名为 deploymentPropertyBag
的 Dictionary<string,string>。通过查看上面 Fiddler 中看到的值,我们可以看到需要放入其中的内容。我建议使用 URL 解码器,例如 http://www.url-encode-decode.com/,使其更易读。解码后,我们得到以下 deploymentPropertyBag 的值。它是一个带有“Key”:“Value”对的字典(此处为了便于阅读添加了换行符):
"
ReleaseName
":"MyReleaseName","
ReleaseBuild
":"MyCompany.MyApplication_1.0.1","
ReleaseBuildChangeset
":"","
TargetStageId
":"3","
MyComponent1:Build
":"","
MyComponent1:BuildChangesetRange
":"-1,-1","
MyComponent2:Build
":"","
MyComponent2:BuildChangesetRange
":"-1,-1"}
通过对反编译的 RM Dll 进行更深入的研究,我找到了 PropertyBagConstants
类,其中包含一些与这些字典键匹配的 const string 属性。以下是我通过实践找到的关于这些属性的每个属性的信息(仅通过试验):
ReleaseName
- 此单个发布的名称。ReleaseBuild
- 当发布模板与 TFS 构建定义关联时使用此项,它是 TFS 的构建号。ReleaseBuildChangeset
- 我还没有看到它的用途。可以将其从 propertyBag 中排除。TargetStageId
- 您需要找出目标阶段的 ID(应该就是您在 Fiddler 中看到的内容)。- 每个组件都有需要设置的属性。在我的示例中,有两个名为 MyComponent1 和 MyComponent2 的组件。这些属性是:
Build
- 对于“通过应用程序构建”组件,这需要是一个空字符串,如上所示。对于“外部构建”组件,这是创建发布时在 UI 中需要输入的值。BuildChangesetRange
- 我还没有看到它的用途。 可以将其从 propertyBag 中排除。
所以,要以编程方式创建草稿模式下的发布以匹配手动完成的操作:
var propertyBag = new Dictionary<string, string>();
propertyBag.Add(PropertyBagConstants.ReleaseName, "MyReleaseName");
propertyBag.Add(PropertyBagConstants.TargetStageId, "3");
propertyBag.Add(PropertyBagConstants.ReleaseBuild, "MyCompany.MyApplication_1.0.1");
var componentNames = new List<string>() { "MyComponent1", "MyComponent2" };
foreach (string componentName in componentNames)
{
propertyBag.Add(String.Format("{0}{1}", componentName, PropertyBagConstants.Build), "");
}
int releaseId = orchestratorService.CreateRelease("MyReleaseTemplate", propertyBag);
现在我们的发布处于草稿模式,是时候修改自定义配置变量了。如果我们手动修改该值并观察 Fiddler,我们可以看到它调用了 ReleaseV2Service/SetRelease。它不传递任何参数,但请求的内容是一个代表发布的巨大 XML 块(根节点是“ReleaseV2”)。让我们在 C# 中获取对象并查看智能感知显示的内容:
IReleaseV2Service releaseService = ReleaseV2ServiceFactory.Instance;
智能感知告诉我们 SetRelease 方法接受一个 XML 字符串(毫不奇怪),这意味着我们需要一种方法来获取发布的当前 XML 以修改它,最好使用类型安全的对象而不是直接修改 XML。
在 Fiddler 中查看(在修改之前)打开发布时,我们可以看到 ReleaseV2Service 调用了 GetRelease 方法。在 C# 中查看该方法,我们看到它接受一个整数 releaseId
并返回 XML。我在(反编译的 RM 中)找到一个 XmlHelper
类,它可以帮助我们进行序列化,以便我们可以使用类型安全的 ReleaseV2
对象。如果我们查看 GetRelease 方法返回的 XML,我们会看到 ReleaseV2 节点被包装在一个 Result 节点中。所以这里有一种方法可以获取我们想要的对象:
string xml = releaseService.GetRelease(releaseId);
string releaseXML = xml.Replace("<Result>", "").Replace("</Result>", "");
ReleaseV2 release = XmlHelper.ToObject<ReleaseV2>(releaseXML);
要弄清楚我们需要深入哪个属性来查找我们的自定义配置变量,我发现最好的方法是在 XML 中搜索它,并设置一个断点来检查我们的 release
对象,以验证它是否在我们期望的位置,这与它的 XML 节点相匹配。事实证明,我们正在寻找的是类似 release.Stages[0].Activities[0].PropertyBagVariables[0]
的东西。但是,我们要修改所有实例的属性,所以我们查找该属性的代码如下:
foreach (IDeploymentEditorStage stage in release.Stages)
{
foreach (StageActivity activity in stage.Activities)
{
ConfigurationVariable applicationPathRootVariable = activity.PropertyBagVariables
.FirstOrDefault(pbv => pbv.Name == PropertyBagConstants.ApplicationPathRoot.ToString());
if (applicationPathRootVariable != null)
{
applicationPathRootVariable.Value = String.Format(@"C:\Windows\DtlDownloads\Release{0}", releaseId);
}
}
}
现在我们有了修改后的 release
对象,我们只需要将其序列化为 XML 并传递给 SetRelease。
string newXML = XmlHelper.ToXml(release);
string result = releaseService.SetRelease(newXML);
然后,我们可以验证草稿是否符合我们的预期,并在单击“启动发布”按钮时检查 Fiddler。它调用 OrchestratorService/StartRelease,因此这段代码非常简单:
orchestratorService.StartRelease(releaseId);
所以,当我们把所有代码放在一起(并添加一些注释)时,我们就得到了:
using Microsoft.TeamFoundation.Release.Common.Helpers;
using Microsoft.TeamFoundation.Release.Data;
using Microsoft.TeamFoundation.Release.Data.Model;
using Microsoft.TeamFoundation.Release.Data.Proxy.Definition;
using Microsoft.TeamFoundation.Release.Data.Proxy.Factory;
using System;
using System.Collections.Generic;
using System.Linq;
namespace MyCompany.ReleaseManagementAPI.Console
{
class Program
{
static void Main(string[] args)
{
IOrchestratorService orchestratorService = OrchestratorServiceFactory.Instance;
IReleaseV2Service releaseService = ReleaseV2ServiceFactory.Instance;
// Build the propertyBag for the draft
var propertyBag = new Dictionary<string, string>();
propertyBag.Add(PropertyBagConstants.ReleaseName, "MyReleaseName");
propertyBag.Add(PropertyBagConstants.TargetStageId, "3");
propertyBag.Add(PropertyBagConstants.ReleaseBuild, "MyCompany.MyApplication_1.0.1");
var componentNames = new List<string>() { "MyComponent1", "MyComponent2" };
foreach (string componentName in componentNames)
{
propertyBag.Add(String.Format("{0}{1}", componentName, PropertyBagConstants.Build), "");
}
// Create the draft
int releaseId = orchestratorService.CreateRelease("MyReleaseTemplate", propertyBag);
// Get the ReleaseV2 object
string xml = releaseService.GetRelease(releaseId);
string releaseXML = xml.Replace("<Result>", "").Replace("</Result>", "");
ReleaseV2 release = XmlHelper.ToObject<ReleaseV2>(releaseXML);
// Find and modify any custom configuration variables named "ApplicationPathRoot"
foreach (IDeploymentEditorStage stage in release.Stages)
{
foreach (StageActivity activity in stage.Activities)
{
ConfigurationVariable applicationPathRootVariable = activity.PropertyBagVariables
.FirstOrDefault(pbv => pbv.Name == PropertyBagConstants.ApplicationPathRoot.ToString());
if (applicationPathRootVariable != null)
{
applicationPathRootVariable.Value = String.Format(@"C:\Windows\DtlDownloads\Release{0}", releaseId);
}
}
}
// Save the changes to the draft
string newXML = XmlHelper.ToXml(release);
string result = releaseService.SetRelease(newXML);
// Start the Release
orchestratorService.StartRelease(releaseId);
}
}
}
另一个小例子
我曾经玩过自动生成我的 Stage Type 选择列表的想法。我使用 Fiddler 获取了所有选择列表的 XML。
string pickListListXML = Services.ConfigurationService.ListPickLists("<Filter />");
该 XML 的根节点是 PickListList,这不是我能找到的类型(它可能不存在)。XMLHelper.ToObject 方法有一个重载,它指定了一个 rootNodeName
,所以我们可以使用它来实现类型安全。
public static string GenerateTargetStagesEnum()
{
string pickListListXML = Services.ConfigurationService.ListPickLists("<Filter />");
// PickListList is the root node name, but that Type does not
// actually exist, so explicitly specify it.
List<PickList> pickLists = XmlHelper.ToObject<List<PickList>>(pickListListXML, "PickListList");
PickList stageTypesList = pickLists.Single(pl => pl.Name == "Stage Type");
// The XML returned by ListPickLists does not contain the actual items in the PickLists
string stageTypesXML = Services.ConfigurationService.GetPickList(stageTypesList.Id);
PickList stageTypesListWithItems = XmlHelper.ToObject<PickList>(stageTypesXML);
// Build a string that contains the enum declaration
var sb = new StringBuilder();
sb.AppendLine("public enum CustomStageType");
sb.AppendLine("{");
foreach (PickList.PickListItem item in stageTypesListWithItems.Items.OrderBy(i => i.Id))
{
sb.AppendLine(String.Format("\t{0} = {1},", item.Name, item.Id));
}
sb.AppendLine("}");
string targetStagesEnum = sb.ToString();
return targetStagesEnum;
}
XmlDocument 示例 - Release Status
我确实遇到了一个问题,它不允许我像在其他示例中那样使用 XmlHelper。我想创建一个方法,该方法可以根据给定的构建号提供发布的环境和状态。
所以,让我们开始吧:
public static Tuple<string, string> GetEnvironmentStatus(string buildNumber)
{
// I use build numbers in the format {ProductName}.Releasing.Deployment_{Version}
string productName = buildNumber.Split('.')[0];
// Get all releases, filtered for the product name
// Strip off the Result node before serialization
var releasesXML = ReleaseV2ServiceFactory.Instance.ListReleases(String.Format("<Filter Name=\"{0}\" />", productName))
.Replace("<Result>", "")
.Replace("</Result>", "");
当我查看上面生成的 XML 时,我看到每个 Release 都有一个“CurrentStageTypeName”属性,其中包含我想要的数据。但是,ReleaseV2 类没有相应的属性。我本可以使用 CurrentStageId 属性来查找名称,但这将是另一次网络通信,所以我决定使用 XmlDocument 来导航 XML。
XmlDocument doc = new XmlDocument();
doc.LoadXml(releasesXML);
XmlNode root = doc.DocumentElement;
foreach (XmlElement node in root.ChildNodes)
{
if (node.Attributes["Build"].Value.ToLower() == buildNumber.ToLower())
{
string stageName = node.Attributes["CurrentStageTypeName"].Value;
string statusName = node.Attributes["StatusName"].Value;
if (stageName == "Prod" && node.Attributes["CurrentStageStepTypeName"].Value == "Accept Deployment")
{
// It was deployed to QA, but has not yet been approved for production
stageName = "QA";
statusName = "Released";
}
return new Tuple<string, string>(stageName, statusName);
}
}
return null;
}
不幸的是,我不得不采取这种方法,但对于这种简单的情况来说,这也不是什么大问题。
摘要
虽然可能需要一些额外的努力来导航此 API,但我希望本文能使您能够使用您需要的那些部分。
历史
2015 年 9 月 21 日 - 我的第一篇文章,第一版。