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:
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:
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:
- Browse to the Json.NET package on nuget.org
- 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. - Rename the
.nupkg
file to end in.zip
and extract its contents. - From the
\lib\net45\
folder, copy theNewtonsoft.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 🙂
5 comments
I did not experiance the problem u explained that could be resolved with the newtonsoft.json. Instead when u use $json = ConvertTo-Json -InputObject $temp -Depth 999 it will get accepted.
ericksegaar.com
Erick Segaar
Can you create a new build definition using powershell in tfs?
If yes, can you please provide an example?
Thanks,
Parag.
Parag Shah
Thank you so much Léon, this helped me a great deal! Much appreciated 🙂
Morné Kruger
In the section Requesting a JSON build definition in the second set of PS code a reference is made to $buildDefinitionUrl. I see it does state “This entry contains the url from which we can request the actual build definition”. I don’t see where the example actually sets this value. Would this be the same value as the $baseUrl that was previously referenced? If not, could I get a little more explanation what the value of $buildDefinitionUrl would look like?
Thanks in advance
kgkdidd
kgkidd
Thank you so much Leon for supplying the script with explanation
Tanvver