In this post, I’ll show how to use the TFS2015 REST API from Powershell to update build definitions, in this case to modify build variables. The TFS2015 REST API is well documented and consists of several areas, of which we’ll be using the build definitions API.

By the way, the TFS2015 Web Portal uses this same REST API as well, so a good way to get to know your way around the API is to monitor the network traffic (for example, using your browser’s developer toolbar) while you’re clicking through the portal to see which parts of the REST API are being accessed.

I’m using TFS2015 Update 3 in combination with Powershell 5.0 and Json.NET 9.0.1.

Requesting a JSON build definition

We’ll start by piecing together the url that contains an overview of all build definitions, and perform a GET request. The -UseDefaultCredentials switch means that the credentials of the currently logged on user should be used to authenticate against TFS.

$baseUrl = "https://tfs.mycorp.net/tfs"
$targetCollection = "DefaultCollection"
$targetProject = "Acme"
$targetBuildName = "Acme - My Build Definition"
$definitionsOverviewUrl = "$baseUrl/$targetCollection/$targetProject/_apis/build/Definitions"

$definitionsOverviewResponse = Invoke-WebRequest -UseDefaultCredentials -Uri $definitionsOverviewUrl

This provides us with an overview of all build definitions. The $definitionsOverviewResponse.Content contains a JSON string that, when displayed in Visual Studio, looks like the following:

BuildDefinitionOverview

Next, we’ll use ConvertFrom-Json to convert the JSON string to an object representation of this information, so that we can locate the build definition entry with the name we’re looking for. This entry contains the url from which we can request the actual build definition:

$definitionsOverview = (ConvertFrom-Json $definitionsOverviewResponse.Content).value
$definitionUrl = ($definitionsOverview | Where-Object { $_.name -eq $targetBuildName } | Select-Object -First 1).url

$response = Invoke-WebRequest $buildDefinitionUrl -UseDefaultCredentials

And indeed, we have received a JSON string with the build definition:

BuildDefinitionResponse

Using Newtonsoft.Json from Powershell

Although the standard ConvertFrom-Json and ConvertTo-Json cmdlets are fine for basic JSON querying, when used to convert a JSON build definition to a PSCustomObject and then back to a JSON string again, the resulting JSON string is not accepted by the TFS2015 rest endpoint – it could have something to do with the behaviour discussed here on UserVoice.

So instead, we’ll use the widely-known Newtonsoft.Json library to do the manipulation with. To use it from your Powershell script, do the following:

  1. Browse to the Json.NET package on nuget.org
  2. Click the “Download” link in the left area to download the raw nuget package file. This .nupkg file is nothing more than a .zip file with a specific folder structure.
  3. Rename the .nupkg file to end in .zip and extract its contents.
  4. From the \lib\net45\ folder, copy the Newtonsoft.Json.dll file and place it in the same directory as where you’re developing your Powershell script.

Now we can load the Newtonsoft.Json.dll into our powershell session and use its JsonConvert method. When using the non-generic overload, it converts the JSON data to an object model of JObject, JArray, etc. instances.

# This assumes the working directory is the location of the assembly:
[void][System.Reflection.Assembly]::LoadFile("$pwd\Newtonsoft.Json.dll")
$buildDefinition = [Newtonsoft.Json.JsonConvert]::DeserializeObject($response.Content)

Note that if you write $buildDefinition to stdout, by default JObject‘s IEnumerable implementation will cause Powershell to treat it like an array and display *all* properties of the object model, which is not really intuitive. To display the contents of a JObject as JSON, you should explicitly invoke its ToString() method, i.e.:

$buildDefinition.ToString()

Modifying a build variable and updating the build definition

Now that we have the build definition in an object model, we can start manipulating it. For example, suppose we have a build variable named “MajorMinor” which contains the major and minor parts of the version to be used as the build number. Displaying its current value and updating it becomes:

# JObjects implement IDictionary and therefore support dot notation
$buildDefinition.variables.MajorMinor.value.ToString()
$buildDefinition.variables.MajorMinor.value = "3.4"

The modified $buildDefinition can now be serialized to JSON again:

$serialized = [Newtonsoft.Json.JsonConvert]::SerializeObject($buildDefinition)

Updating the build definition is done by uploading the JSON document to the same build definition url using a HTTP PUT. However, Invoke-WebRequest appears to perform some strange string mangling when it encounters special characters (such as the “é” from my name), and I found that one way to circumvent that is to perform the encoding to UTF-8 ourselves and instead pass it the raw byte data:

$postData = [System.Text.Encoding]::UTF8.GetBytes($serialized)

# The TFS2015 REST endpoint requires an api-version header, otherwise it refuses to work properly.
$headers = @{ "Accept" = "api-version=2.3-preview.2" }
$response = Invoke-WebRequest -UseDefaultCredentials -Uri $buildDefinitionUrl -Headers $headers `
              -Method Put -Body $postData -ContentType "application/json"
$response.StatusDescription

Uploading the updated build definition should now succeed with an “Ok” result.

Modifying multiple build variables

This seems a bit cumbersome to just update a single variable, but obviously, the real power lies in being able to update multiple build definitions at once. For example, to update the MajorMinor variable of all build definitions that have “Acme” in their name, you can do something like the following:

[void][System.Reflection.Assembly]::LoadFile("$pwd\Newtonsoft.Json.dll")

$baseUrl = "https://tfs.mycorp.net/tfs"
$targetCollection = "DefaultCollection"
$targetProject = "Acme"
$majorMinor = "3.5"

# Get an overview of all build definitions in this team project
$definitionsOverviewUrl = "$baseUrl/$targetCollection/$targetProject/_apis/build/Definitions"
$definitionsOverviewResponse = Invoke-WebRequest -UseDefaultCredentials -Uri $definitionsOverviewUrl
$definitionsOverview = (ConvertFrom-Json $definitionsOverviewResponse.Content).value

# Process all builds that have "Acme" in their name
foreach($definitionEntry in ($definitionsOverview | Where-Object { $_.name -like '*Acme*' }))
{
    $definitionUrl = $definitionEntry.url
    $response = Invoke-WebRequest $buildDefinitionUrl -UseDefaultCredentials
    $buildDefinition = [Newtonsoft.Json.JsonConvert]::DeserializeObject($response.Content)

    # If the build has a MajorMinor variable, update it.
    if($buildDefinition.variables.MajorMinor)
    {
        Write-Output "Updating build ""$($definitionEntry.name)""..."

        $buildDefinition.variables.MajorMinor.value = $majorMinor

        $serialized = [Newtonsoft.Json.JsonConvert]::SerializeObject($buildDefinition)
        $postData = [System.Text.Encoding]::UTF8.GetBytes($serialized)

        $headers = @{ "Accept" = "api-version=2.3-preview.2" }
        $response = Invoke-WebRequest -UseDefaultCredentials -Uri $buildDefinitionUrl `
                       -Headers $headers -Method Put -Body $postData `
                       -ContentType "application/json"
        $response.StatusDescription
    }
}

Obviously, in the same way you could also change paths or arguments in existing build steps, or add new steps.
Again, the sky’s the limit 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *