Are TFS Build Agent User Capabilities' Values

2019-02-18 01:38发布

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?

2条回答
淡お忘
2楼-- · 2019-02-18 02:12

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).

查看更多
Fickle 薄情
3楼-- · 2019-02-18 02:17

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.

查看更多
登录 后发表回答