Handling builds via Team Foundation Server 2017 REST API

By Mirek on (tags: build, HttpClient, REST, tfs, categories: infrastructure, web)

In this post I am going to show you how to utilize the new TFS 2017 REST API. In particular you will see how easy is to list, queue and manage builds for your team project.

The plan is to

  1. Authorize to TFS 2017 on premise and connect to its web rest api.
  2. Get a list of team projects
  3. For particular team project get a list of build definitions
  4. Queue particular build definition
  5. Check the status of the build

For that I’m going to write a simple web application, handle all of those tasks in the controller actions and display result on simple page.

Please keep in mind this is just a sample implementation and should not be used in production in this form. There is no parameters validation or exception handling. The goal is only to show how to call and consume the TFS apis.

The REST API of Visual Studio Team Services and Team Foundation Server is widely documented here.

1. Connecting to the TFS REST API

The apis support OAuth authentication and this is the recommended way to go, but you can also use a user password authentication. More on that here and here.

Let’s start by creating a HttpClient

private HttpClient CreateClient()
{
    var client = new HttpClient();
    client.BaseAddress = new Uri(TfsURL);
    client.DefaultRequestHeaders.Accept.Clear();
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic",
        Convert.ToBase64String(Encoding.ASCII.GetBytes(string.Format("{0}:{1}", "", PAT))));
    client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
    return client;
}

We start with the base address, which will be

string TfsURL = "http://my.tfs.com/DefaultCollection/";

Of course instead of my.tfs.com you should place a valid TFS server name. The PAT variable holds the personal access token which has to be included in a base 64 encoded form in the request headers. Remember do not hardcode the personal access token and keep it in a safe location. To obtain an access token for your TFS account follow these instructions.

2. Retreiving a list of team projects

According to this we first prepare a proper url to get a list of all team projects from the collection

string ProjectsURL = "_apis/projects?api-version=2.0";

Then for the sake of simplicity we say we only want to retrieve the project name and id, so create a simple model for representing this

public class TeamProject
{
    public string Id { get; set; }
    public string Name { get; set; }
}

Although the response objects returned by the TFS contains much more properties, we can deserialize only those which we are interested in. That’s all thanks to the json format.

public async Task<ActionResult> Index()
{
    var client = CreateClient();
    TFSReponse<TeamProject> projects = null;
    HttpResponseMessage response = await client.GetAsync(ProjectsURL);
    if (response.IsSuccessStatusCode)
    {
        projects = await response.Content.ReadAsAsync<TFSReponse<TeamProject>>();
    }
    return View(projects.Value);
}

The TFSResponse is a simple class that wraps the api response

public class TFSReponse<T> where T : class
{
    public int Count { get; set; }
    public List<T> Value { get; set; }
}

I will intentionally skip the view part of the project to keep the post lean.

3. List build definitions

Build definitions api is documented here. First we start with a model class for the build definition.

public class BuildDefinition
{
    public string Id { get; set; }
    public string Name { get; set; }
    public string Type { get; set; }
}

You can of course extend it according to your needs, but to trigger a build nothing more that a build definition id is required.

The url that returns a build definitions for particular project looks like this

string ProjectDefinitionsURL = "{0}/_apis/build/definitions?api-version=2.0&type=build";

One comment here. The type query parameter tells the TFS to return only definitions of the new build type. The other value that can be passed here is xaml, which causes to return the old deprecated xaml build definitions. If nothing is specified all definitions are returned. As you probably noticed this is a string pattern. The {0} parameter must be replaced by the project name or id.

Having that we can retrieve the list of build definitions for the project

public async Task<PartialViewResult> GetBuilds(string teamProjectName)
{
    var client = CreateClient(); 
    TFSReponse<BuildDefinition> definitions = null;
    HttpResponseMessage response = await client.GetAsync(string.Format(ProjectDefinitionsURL, teamProjectName));
    if (response.IsSuccessStatusCode)
    {
        definitions = await response.Content.ReadAsAsync<TFSReponse<BuildDefinition>>();
    }
    ViewBag.TeamProjectName = teamProjectName;
    return PartialView("_BuildDefinitions", definitions.Value);
}

4. Triggering a build

According to the description to trigger a build we need to do a POST call to the api and pass a json object with build definition details. We have here a full set of options, so we can run a build as we would do it from the web portal of the TFS.

string PostBuildURL = "{0}/_apis/build/builds?api-version=2.0";

The action method looks like this

public async Task<PartialViewResult> RunBuild(string teamProjectName, string definitionId)
{
    var client = CreateClient();
    var buildContent = new { definition = new { id = definitionId } };
    var buildjson = JsonConvert.SerializeObject(buildContent);
 
    var httpContent = new StringContent(buildjson, Encoding.UTF8, "application/json");
 
    BuildStatus status = null;
    HttpResponseMessage response = await client.PostAsync(string.Format(PostBuildURL, teamProjectName), httpContent);
    if (response.IsSuccessStatusCode)
    {
        status = await response.Content.ReadAsAsync<BuildStatus>();
    }
    ViewBag.TeamProjectName = teamProjectName;
    return PartialView("_BuildStatus", status);
}

First we build the body of the message, which has to be a json string. I’ve decided to build an anonymous object buildContent and serialize it to json. Here we can add also a parameters property with a collection of key value pairs, such as “system.debug”:”true” or any other acceptable by the build definition on trigger.

Then we pass that json object as a body to the request and call the api. The response is quite huge, as you can see in the documentation, but lets extract only two of its properties.

public class BuildStatus
{
    public string Id { get; set; }
    public string Status { get; set; }
}
The Id is the identifier of the build, the Status is a string which says: notStarted, inProggress, completed and so on.

5. Get the status of the build

To retrieve the status of the build we use following url

string BuildStatusURL = "{0}/_apis/build/builds/{1}?api-version=2.0";

where {0} is again a placeholder for team project name and {1} must be filled with the build id.

Calling the api here is straight forward

public async Task<PartialViewResult> GetBuildStatus(string teamProjectName, string buildId)
{
    var client = CreateClient();
 
    BuildStatus status = null;
    HttpResponseMessage response = await client.GetAsync(string.Format(BuildStatusURL, teamProjectName, buildId));
    if (response.IsSuccessStatusCode)
    {
        status = await response.Content.ReadAsAsync<BuildStatus>();
    }
    ViewBag.TeamProjectName = teamProjectName;
    return PartialView("_BuildStatus", status);
}

 

That’s it. As you can see working with TFS REST API is quite easy. It allows you to fully integrate with the TFS and use all of its benefits outside the web portal or Visual Studio.

Whole project for this post can be found in the attachment below.