Introduction
The main point of this article is to provide you with the ability to use the Release Management API, even to do things that are not explicitly discussed in this article. It may be undocumented, but it is not undiscoverable.
Supposedly, the documentation will be located at https://www.visualstudio.com/en-us/integrate/api/overview when it does become available. There is also a UserVoice item asking Microsoft to provide that official documentation.
The example I will be demonstrating is how to create a release in draft mode, edit the value of a custom configuration variable for that specific release, and start that release.
When I was researching this topic, the examples I found only explained how to either
- Use Powershell to trigger a release without modifying it using the WebAPI
- Use C# to do various things (including trigger a release), but in a way that seems hard to discover how to use features that are not explicitly in the example.
Requirements
To use the Release Management API, you will need to have access to the Release Management Client and its installation directory.
To be able to navigate the API yourself to figure out how to do things not explicitly mentioned in this article, you will need to have Fiddler installed (it's free).
Background for Specific Example
The example in this articles uses a vNext release template with 2 components (MyComponent1 and MyComponent2). Both components are set as "Builds with Application".
Out of the box, vNext templates that use the same component and the same machine cannot be used in simultaneous releases due to the fact that they will try to place their files in the exact same directory.
I needed run a portion of my deployment on the same machine for all applications, and I did not want to create a separate component for each application at each stage.
Release Management provides several locations to define variables, and it has a system variable called ApplicationPathRoot that you can override to change the destination path where the component it copied. Details for that are located at https://msdn.microsoft.com/en-us/library/dn834972(v=vs.120).aspx.
The only place to override the ApplicationPathRoot value per release is to add a custom configuration variable called ApplicationPathRoot to each action in your template.
This example is for RM 2015, but the concepts should work in any version.
Setting up the Solution
First, we need to create a new C# project. A simple console application is good enough for this example.
The actual API dlls that we will need to reference are located in the application directory of the Release Management Client install (for me, this is C:\Program Files (x86)\Microsoft Visual Studio 14.0\Release Management\Client\bin). The dlls that we will need from this folder are:
- Microsoft.TeamFoundation.Release.Common
- Microsoft.TeamFoundation.Release.CommonResources
- Microsoft.TeamFoundation.Release.Data
- Microsoft.TeamFoundation.Release.Data2
- Microsoft.TeamFoundation.Release.Data3
- Microsoft.Practices.EnterpriseLibrary.Validation
We will also need to add a reference to:
- WindowsBase.dll.
We need to configure our console app to point to our Release Management Server. To do this, copy the contents of Microsoft.TeamFoundation.Release.Data.dll.config (from the RM Client application folder) into our console app's App.config. NOTE: The "supportedRuntime" section should NOT be copied to our App.config, or else the app might not launch properly.
Figuring Out Where to Start
Now that we have the solution set up, how do we know what Type we need to reference inside our code? You could jump down to my code example below, but if you want to learn how to figure it out for yourself, then open up Fiddler.
Fiddler lets us see all traffic going to and from your computer. Since the Release Management Client communicates to the server via a WebAPI, we can do something manually in the Client and Fiddler will tell us what the WebAPI equivalent is, which maps pretty closely with the .NET API.
So for our example, create a Release in draft mode in the Client. Fiddler will show a few (19 for me) requests (after you click anything in RM, there are several requests worth of loading the next screen). We need to flip through the requests, looking in the Request Header to find something that sounds like what we want. Here are a few of the Request Headers that I see after creating a Release in draft:
-
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
First, notice how each of these requests have a similar layout:
/account/releaseManagementService/_apis/releaseManagement/{ServiceName}/{MethodName}?{Parameters}.
That CreateRelease method sounds like what we want, and the service that provides that is the OrchestratorService.
What I found out (from poking around in the decompiled RM dlls) is that if we want a ServiceName instance, we need to get an IServiceName object one of 2 ways (which one depends on which service is needed):
- {ServiceName}Factory.Instance
- Services.{ServiceName}
So what we need in this case is:
IOrchestratorService orchestratorService = OrchestratorServiceFactory.Instance;
Writing the Code
Now that we have the instance, we can use intellisense to find that it has a CreateRelease method that takes the same parameters we saw above! The first parameter is releaseTemplateName
, which is obviously the name of the Release Template that we want to use to create our Release.
The second parameter is not as obvious though. It's a Dictionary<string,string> called deploymentPropertyBag
. We can see the sorts of things that need to go in it by looking at the value we saw in Fiddler above. I would suggest using a URL decoder such as http://www.url-encode-decode.com/ to make it more readable. After decoding it, we end up with the following for the value of deploymentPropertyBag. It is a dictionary with "Key":"Value" pairs (line breaks added here for easier reading):
{
"ReleaseName
":"MyReleaseName",
"ReleaseBuild
":"MyCompany.MyApplication_1.0.1",
"ReleaseBuildChangeset
":"",
"TargetStageId
":"3",
"MyComponent1:Build
":"",
"MyComponent1:BuildChangesetRange
":"-1,-1",
"MyComponent2:Build
":"",
"MyComponent2:BuildChangesetRange
":"-1,-1"
}
With more digging in the decompiled RM dlls, I found the PropertyBagConstants
class with some const string properties that match up to those dictionary keys. Here is what I have found about each of these properties (just from playing around with them):
ReleaseName
- The name of this individual release. ReleaseBuild
- This is used when the release template is tied to a TFS build definition, and is the TFS build number. ReleaseBuildChangeset
- I have not seen where this is used. It can be left out of the propertyBag. TargetStageId
- You will have to figure out the ID of the target stage you want (it should be whatever you saw in Fiddler). - Each component has properties that need to be set as well. In my example, there are 2 components named MyComponent1 and MyComponent2. The properties on these are:
Build
- For "Builds with application" components, this needs to be an empty string as above. For "Builds externally" components, this is the value that would have to be entered in the UI when creating a release. BuildChangesetRange
- I have not seen where this is used. It can be left out of the propertyBag.
So, to programmatically create the release in draft to match what was manually done above:
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);
Now that our release is in draft mode, it's time to modify the custom configuration variable. If we modify the value manually and watch Fiddler, we can see that it calls a ReleaseV2Service/SetRelease. It does not pass any parameters, but the content of the request is a large block of XML that represents the release (the root node is a "ReleaseV2"). Let's get the object in C# and see what intellisense shows us:
IReleaseV2Service releaseService = ReleaseV2ServiceFactory.Instance;
Intellisense tells us that the SetRelease method takes a string of XML (no surprise there), which means we need a way to get the release's current XML to modify it, preferably using type-safe objects instead of directly modifying the XML.
Looking in Fiddler when we open the release (before modifying it), we can see that the ReleaseV2Service calls a GetRelease method. Looking at that method in C#, we see that it takes an integer releaseId
and returns XML. I found (in my decompiled RM) an XmlHelper
class that can help us with our serialization so that we can work with a type-safe ReleaseV2
object. If we look at the XML returned by the GetRelease method, we will see that the ReleaseV2 node is wrapped in a Result node. So here is one way we can get the object we want:
string xml = releaseService.GetRelease(releaseId);
string releaseXML = xml.Replace("<Result>", "").Replace("</Result>", "");
ReleaseV2 release = XmlHelper.ToObject<ReleaseV2>(releaseXML);
To figure out what property we need to drill down into to find our custom configuration variable, the best way I found was to search for it in the XML, and set a breakpoint to inspect our release
object to verify that it's where we expect based on its XML nodes. It turns out that what we are looking for is in something like release.Stages[0].Activities[0].PropertyBagVariables[0]
. We want to modify all instances of the property though, so our code to look for this would be:
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);
}
}
}
Now that we have our modified release
object, we just need to serialize it to XML and pass it to SetRelease.
string newXML = XmlHelper.ToXml(release);
string result = releaseService.SetRelease(newXML);
We can then verify that the draft looks like we want, and inspect Fiddler as we click the Start Release button. It calls OrchestratorService/StartRelease, so the code for this is pretty simple:
orchestratorService.StartRelease(releaseId);
So, when we put all of the code together (and add some comments), we get:
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;
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);
string xml = releaseService.GetRelease(releaseId);
string releaseXML = xml.Replace("<Result>", "").Replace("</Result>", "");
ReleaseV2 release = XmlHelper.ToObject<ReleaseV2>(releaseXML);
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);
}
}
}
string newXML = XmlHelper.ToXml(release);
string result = releaseService.SetRelease(newXML);
orchestratorService.StartRelease(releaseId);
}
}
}
Another Small Example
I was playing with the idea of auto-generating my Stage Type pick list. I used Fiddler to get the XML of all pick lists:
string pickListListXML = Services.ConfigurationService.ListPickLists("<Filter />");
The root node of that XML was a PickListList, which is not a type that I was able to find (it may not exist). There is an overload of the XMLHelper.ToObject method that specifies a rootNodeName
, so we can use that to achieve type-safety.
public static string GenerateTargetStagesEnum()
{
string pickListListXML = Services.ConfigurationService.ListPickLists("<Filter />");
List<PickList> pickLists = XmlHelper.ToObject<List<PickList>>(pickListListXML, "PickListList");
PickList stageTypesList = pickLists.Single(pl => pl.Name == "Stage Type");
string stageTypesXML = Services.ConfigurationService.GetPickList(stageTypesList.Id);
PickList stageTypesListWithItems = XmlHelper.ToObject<PickList>(stageTypesXML);
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 Example - Release Status
I did run into something that did not allow me to use the XmlHelper like I did in the other examples. I wanted to create a method that would gave me the current environment and status of a release from a given build number.
So to get started:
public static Tuple<string, string> GetEnvironmentStatus(string buildNumber)
{
string productName = buildNumber.Split('.')[0];
var releasesXML = ReleaseV2ServiceFactory.Instance.ListReleases(String.Format("<Filter Name=\"{0}\" />", productName))
.Replace("<Result>", "")
.Replace("</Result>", "");
When I viewed the XML generated above, I saw that each Release had a "CurrentStageTypeName" property that contained the data I wanted. However, the ReleaseV2 class does not have a corresponding property. I could have used the CurrentStageId property to look up the name, but that would have been another call across the wire so I decided to navigate the XML with an XmlDocument instead.
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")
{
stageName = "QA";
statusName = "Released";
}
return new Tuple<string, string>(stageName, statusName);
}
}
return null;
}
It's unfortunate that I had to resort to this, but not too big of a deal for a simple scenario like this.
Summary
While it may take some extra work to navigate this API, I hope that this article will enable you to work with the piece that you need.
History
9/21/15 - My first article, first version.