4

I'm trying to write a build step within TFS that relies on knowing where the Build agent has nuget.exe stored (the standard nuget-install step mucks around with the order of arguments in a way that breaks build execution, so I want to run the exe myself using one of the batch/shell/ps steps).

It would seem that setting up a capability on the Build Agent with that path would make sense, but I cannot seem to reference the value in any of my build steps, and I cannot find anything helpful on MSDN.

I'm expecting it to be something like $(Env.MyUserCapability), but it never resolves to the value.

Is it possible to retrieve a capability value within a build step? And if so, how do you do it? And if not, what is a viable alternative?

Izzy
  • 1,764
  • 1
  • 17
  • 31

2 Answers2

2

The user-defined capabilities are metadata only. But you can set a global environment variable (e.g. NUGET) and set that to a path to a nuget.exe, when you restart the agent, the machine-wide environment is then discovered as capability and you can then use it.

If you are writing a custom task, you can also add a nuget.exe to the task that will be downloaded to the executing agent.

Martin Ullrich
  • 94,744
  • 25
  • 252
  • 217
  • Yeah I thought about doing this... then I can use the capability to signal that this global environment variable has been set. It just seems like such a waste to not allow a build step to access the values there... And how do you access it, if you don't mind? – Izzy May 08 '17 at 14:38
  • The included task has an embedded nuget.exe, if you are writing a task (and not just a command step), you could do the same.. – Martin Ullrich May 08 '17 at 14:40
  • %GLOBAL_VAR_NAME% (facepalm) But you can also put it in as a user variable too, if you know what user the build agent runs as. – Izzy May 08 '17 at 15:29
  • yeah sure. i just like them global so that everyone can enter-pssession into the build server to diagnose issues.. – Martin Ullrich May 08 '17 at 15:32
1

UPDATE: I made a public extension out of this.

UPDATE: this works in Azure DevOps 2019.

In TFS 2018u1, the following works:

Import-Module "Microsoft.TeamFoundation.DistributedTask.Task.Common"
Import-Module "Microsoft.TeamFoundation.DistributedTask.Task.Internal"
Add-Type -Assembly "Microsoft.TeamFoundation.DistributedTask.WebApi"

$VSS = Get-VssConnection -TaskContext $distributedTaskContext
$AgentCli = $VSS.GetClient([Microsoft.TeamFoundation.DistributedTask.WebApi.TaskAgentHttpClient])

$AgentConfig = Get-Content "$Env:AGENT_HOMEDIRECTORY\.agent" -Raw | ConvertFrom-Json
$Agent = $AgentCli.GetAgentAsync($AgentConfig.PoolId, $Env:AGENT_ID, $TRUE, $FALSE, $NULL, $NULL, [System.Threading.CancellationToken]::None).GetAwaiter().GetResult()

if($Agent.UserCapabilities.MyCapability)
{
    Write-Host "Got the capability!";
} 

The long string of default arguments ending with CancellationToken::None is for compatibility with Powershell 4. PS4 doesn't support default values for value-typed method parameters, PS5 does.

This snippet does something very questionable - it relies on the location and the structure of the agent configuration file. This is fragile. The problem is that the GetAgentAsync method requires both pool ID and the agent ID, and the former is not exposed in the environment variables. A slightly less hackish approach would check all pools and find the right one by the agent ID:

$Pools = $AgentCli.GetAgentPoolsAsync($NULL, $NULL, $NULL, $NULL, $NULL, [System.Threading.CancellationToken]::None).GetAwaiter().GetResult()
$Demands = New-Object 'System.Collections.Generic.List[string]'
foreach($Pool in $Pools)
{
    $Agent = $AgentCli.GetAgentsAsync($Pool.ID, $Env:AGENT_NAME, $TRUE, $FALSE, $NULL, $Demands, $NULL, [System.Threading.CancellationToken]::None).Result
    if($Agent -and $Agent.Id -eq $Env:AGENT_ID)
    {
        Break
    }
}

This relies on another undocumented implementation detail, specifically that agent IDs are globally unique. This seems to hold as late as TFS 2018, but who knows.


When you employ the $distributedTaskContext, the task is connecting back to TFS with an artificial user identity, "Project Collection Build Service" (not with the agent service account). There's one user like that in each collection, they're distinct. In order to allow tasks running in releases in a collection to query the agent for user capabilities, you need to grant the Reader role to the relevant pool(s) (or all pools) to the user account called "Project Collection Build Service (TheCollectionName)" from that collection.

It also looks like some actions also grant an implicit Reader role on a pool to the task identity.


Alternatively, you can construct a VssConnection from scratch with Windows credentials, and grant the agent account(s) Reader role on the pool(s).

Seva Alekseyev
  • 59,826
  • 25
  • 160
  • 281
  • Is this meant to be a Powershell Script Task? – TGlatzer Dec 12 '18 at 09:28
  • Could be an inline snippet inside a Powershell or a Powershell++ task. – Seva Alekseyev Dec 12 '18 at 14:56
  • I made it a file, since inline snippets may only be 500chars. But, the IPMO's at the beginning throw a Module not found. Do I need specific things installed on the agent? – TGlatzer Dec 12 '18 at 16:27
  • The Powershell++ task is considered legacy, but doesn't have the 500 char limitation :) Just sayin'. That said, all the modules in question should be present on the agent machine, I didn't install them separately. Maybe they're not in path in your scenario? With inline snippets they are. – Seva Alekseyev Dec 12 '18 at 17:11
  • Powershell++ is here: https://marketplace.visualstudio.com/items?itemName=ms-devlabs.utilitytasks – Seva Alekseyev Dec 12 '18 at 17:32