Pipeline extension scoped to a specific TFS or Agent version

Pipeline extension scoped to a specific TFS or Agent version

In The Netherlands more and more cities are creating an artificial wall around the city centers for older cars, certain fuel types or lorries. This is happening elsewhere in Europe as well. A similar things needs to happen in the Azure DevOps marketplace now that the number of supported target server versions is increasing.

As an extension developer it is becoming harder and harder to retain compatibility with all the different TFS versions out there as well as with Azure Devops with its high release frequency.

While Microsoft officially only supports the RTM and the latest Update pack of each major TFS version (2015, 2015.4.1, 2017, 2017.3, 2018.0.1 and 2018.3), many clients are lingering on some version in between 2015 and 2018.3 and wish for your extensions to work with their version.

Up until now I've simply had my extensions target Azure DevOps and TFS. No version limitations. Yet with the introduction of YAML and Output Parameters and more modern versions of the underlying Node runtime, I'm considering adding these features which may break 2015 and 2017 versions of my build tasks.

There are multiple places where a task developer can scope their extensions or tasks. Let's have a quick look at our options:

Ways to target your extension to Azure DevOps and/or TFS

The simplest option you have is to scope the vss-extension.json manifest file to Azure DevOps or TFS using the Installation targets:

Target Available on
Microsoft.VisualStudio.Services.Cloud Azure DevOps
Microsoft.TeamFoundation.Server Team Foundation Server
Microsoft.VisualStudio.Services Azure DevOps
and Team Foundation Server

You can either list support for Azure DevOps and Team Foundation Server explicitly or use the older Visual Studio Services option that still stems from the time Azure DevOps was called Visual Studio Online.

{
    "targets": [
        {
            "id": "Microsoft.VisualStudio.Services.Cloud"
        },
        {
            "id": "Microsoft.TeamFoundation.Server",
        }
    ]
}

Is the explicit equivalent of:

{
    "targets": [
        {
            "id": "Microsoft.VisualStudio.Services"
        }
    ]
}

Ways to target specific versions of Team Foundation Server

In my case I want to support TFS 2018 and Azure DevOps, since TFS 2018 has support for the new Output Variables. In order to exclude older versions of TFS you'll need to add a version to your target. The exact syntax is clearly defined in the docs.

{
    "targets": [
        {
            "id": "Microsoft.VisualStudio.Services.Cloud"
        },
        {
            "id": "Microsoft.TeamFoundation.Server",
            "version": "[16.0,)"
        }
    ]
}

It's better to target API versions instead of Servers

If you're dependent on the availability of a specific API version, then you can add demands to your extension manifest instead of a version range. These demands ensure that the server you're installing into has the required server side APIs available:

{
    "demands": [
        "api-version/3.0"
    ]
}

You can demand the availability of an extension point as well:

{
    "demands": [
        "contribution/ms.vss-dashboards-web.widget-catalog"
    ]
}

The marketplace will compare the demands against the known APIs in Team Foundation Server and will show the correct list of supported servers, even if a demand is added to a later update pack without your knowledge. Nifty eh? The same should go for Preview features.

Unfortunately there is no way to demand specific Build and Release features. You can't demand "Release Gates available" as an option, though you may be able to work around this by looking up some of the UI extension points for these features.
Similarly, you can't demand a minimum agent version for your extension. You can for tasks, but that won't prevent people from installing the extension to a TFS server version that will never be able to run your tasks.

So what about build and release tasks?

Your build task contribution comes with its task.json manifest and you can tweak a few settings there to show support for different versions.

minimumAgentVersion

If you're dependent on a feature in the VSTS Task API that depends on a specific version of the agent, you can set the minimumAgentVersion property in the Task Manifest. A comprehensive list of agent versions and API features can be found int he vsts-task-lib docs.

The same page also lists which versions of the Agent have sipped with which versions of TFS. This will allow you to set the supported target version in the vss-extension.json accordingly.

{
  "minimumAgentVersion": "1.83.0"
}

demands

Another way to handle compatibility checks is through Demands. Demands are matched against the capabilities of the available Agents and against the capabilities provided by other tasks (like Tool Installers).

As a task author you can also specific custom demands that Agent Administrators need to configure on each Agent. This will signal the agent is compatible and will at least force an DevOps Pipelines administrator to look into the compatibility requirements of your task.

A long list of demands is available by default, but unfortunately, you can't perform any logic on them. You can only detect the presence of a demand from your task manifest.

Capability name Capability value
Agent.ComputerName SNAPPIE
Agent.HomeDirectory C:\TfsData\jessehouwing
Agent.Name SNAPPIE
Agent.OS Windows_NT
Agent.OSVersion 10.0.17763
Agent.Version 2.136.1
ALLUSERSPROFILE C:\ProgramData
APPDATA C:\WINDOWS\ServiceProfiles\NetworkService\AppData\Roaming
AzurePS 5.7.0
ChocolateyInstall C:\ProgramData\chocolatey
Cmd C:\WINDOWS\system32\cmd.exe
CommonProgramFiles C:\Program Files\Common Files
CommonProgramFiles(x86) C:\Program Files (x86)\Common Files
CommonProgramW6432 C:\Program Files\Common Files
COMPUTERNAME SNAPPIE
ComSpec C:\WINDOWS\system32\cmd.exe
docker C:\Program Files\Docker\Docker\Resources\bin\docker.exe
DotNetFramework C:\Windows\Microsoft.NET\Framework64\v4.0.30319
DotNetFramework_2.0 C:\Windows\Microsoft.NET\Framework\v2.0.50727
DotNetFramework_2.0_x64 C:\Windows\Microsoft.NET\Framework64\v2.0.50727
DotNetFramework_3.0 C:\Windows\Microsoft.NET\Framework\v3.0
DotNetFramework_3.0_x64 C:\Windows\Microsoft.NET\Framework64\v3.0
DotNetFramework_3.5 C:\Windows\Microsoft.NET\Framework\v3.5
DotNetFramework_3.5_x64 C:\Windows\Microsoft.NET\Framework64\v3.5
DotNetFramework_4.7.0 C:\Windows\Microsoft.NET\Framework\v4.0.30319
DotNetFramework_4.7.0_x64 C:\Windows\Microsoft.NET\Framework64\v4.0.30319
DriverData C:\Windows\System32\Drivers\DriverData
ES_HEAP_SIZE 9600m
FSHARPINSTALLDIR C:\Program Files (x86)\Microsoft SDKs\F#\10.1\Framework\v4.0\
InteractiveSession FALSE
java C:\Program Files (x86)\Java\jre1.8.0_181
java_8 C:\Program Files (x86)\Java\jre1.8.0_181
JAVA_HOME C:\Program Files\Microsoft Team Foundation Server 15.0\Search\Java\jre1.8.0_141
LOCALAPPDATA C:\WINDOWS\ServiceProfiles\NetworkService\AppData\Local
MSBuild C:\Program Files (x86)\Microsoft Visual Studio\Preview\Enterprise\MSBuild\15.0\Bin\
MSBuild_12.0 C:\Program Files (x86)\MSBuild\12.0\bin\
MSBuild_14.0 C:\Program Files (x86)\MSBuild\14.0\bin\
MSBuild_14.0_x64 C:\Program Files (x86)\MSBuild\14.0\bin\amd64\
MSBuild_15.0 C:\Program Files (x86)\Microsoft Visual Studio\Preview\Enterprise\MSBuild\15.0\Bin\
MSBuild_15.0_x64 C:\Program Files (x86)\Microsoft Visual Studio\Preview\Enterprise\MSBuild\15.0\Bin\amd64\
MSBuild_2.0 C:\Windows\Microsoft.NET\Framework\v2.0.50727\
MSBuild_2.0_x64 C:\Windows\Microsoft.NET\Framework64\v2.0.50727\
MSBuild_3.5 C:\Windows\Microsoft.NET\Framework\v3.5\
MSBuild_3.5_x64 C:\Windows\Microsoft.NET\Framework64\v3.5\
MSBuild_4.0 C:\Windows\Microsoft.NET\Framework\v4.0.30319\
MSBuild_4.0_x64 C:\Windows\Microsoft.NET\Framework64\v4.0.30319\
MSBuild_x64 C:\Program Files (x86)\Microsoft Visual Studio\Preview\Enterprise\MSBuild\15.0\Bin\amd64\
MSMPI_BIN C:\Program Files\Microsoft MPI\Bin\
node.js C:\Program Files\nodejs\node.exe
npm C:\Program Files\nodejs\npm.cmd
NUMBER_OF_PROCESSORS 8
OS Windows_NT
PATHEXT .COM,.EXE,.BAT,.CMD,.VBS,.VBE,.JS,.JSE,.WSF,.WSH,.MSC,.RB,.RBW
PowerShell 5.1.17763.1
PROCESSOR_ARCHITECTURE AMD64
ProgramData C:\ProgramData
ProgramFiles C:\Program Files
ProgramFiles(x86) C:\Program Files (x86)
ProgramW6432 C:\Program Files
PSModulePath %ProgramFiles%\WindowsPowerShell\Modules,C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules,C:\Program Files (x86)\Microsoft SQL Server\130\Tools\PowerShell\Modules,C:\Program Files\Microsoft Azure Recovery Services Agent\bin\Modules\
PUBLIC C:\Users\Public
PYTHON C:\Users\JesseHouwing\AppData\Local\Programs\Python\Python36\
SEARCH_ES_INDEX_PATH C:\TfsData\Search\IndexStore
SqlPackage C:\Program Files (x86)\Microsoft SQL Server\140\DAC\bin\SqlPackage.exe
SystemDrive C:
SystemRoot C:\WINDOWS
TEAMCITY_SERVER_OPTS -Djava.library.path=C:\ProgramData\JetBrains\TeamCity\lib\jdbc\sqljdbc_auth.dll
TEMP C:\WINDOWS\ServiceProfiles\NetworkService\AppData\Local\Temp
TMP C:\WINDOWS\ServiceProfiles\NetworkService\AppData\Local\Temp
USERPROFILE C:\WINDOWS\ServiceProfiles\NetworkService
VisualStudio C:\Program Files (x86)\Microsoft Visual Studio\Preview\Enterprise\
VisualStudio_15.0 C:\Program Files (x86)\Microsoft Visual Studio\Preview\Enterprise\
VisualStudio_IDE C:\Program Files (x86)\Microsoft Visual Studio\Preview\Enterprise\Common7\IDE\
VisualStudio_IDE_15.0 C:\Program Files (x86)\Microsoft Visual Studio\Preview\Enterprise\Common7\IDE\
VS140COMNTOOLS C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\Tools\
VSTest C:\Program Files (x86)\Microsoft Visual Studio\Preview\Enterprise\Common7\IDE\CommonExtensions\Microsoft\TestWindow
VSTest_15.0 C:\Program Files (x86)\Microsoft Visual Studio\Preview\Enterprise\Common7\IDE\CommonExtensions\Microsoft\TestWindow
windir C:\WINDOWS
WindowsKit C:\Program Files (x86)\Windows Kits\8.1\
WindowsKit_8.1 C:\Program Files (x86)\Windows Kits\8.1\
WindowsSdk C:\Program Files (x86)\Microsoft SDKs\Windows\v8.1A
WindowsSdk_8.1 C:\Program Files (x86)\Microsoft SDKs\Windows\v8.1A
WindowsSdk_8.1_NetFx40Tools C:\Program Files (x86)\Microsoft SDKs\Windows\v8.1A\bin\NETFX 4.5.1 Tools
WindowsSdk_8.1_NetFx40Tools_x64 C:\Program Files (x86)\Microsoft SDKs\Windows\v8.1A\bin\NETFX 4.5.1 Tools\x64
WIX C:\Program Files (x86)\WiX Toolset v3.11\

And you'd almost expect to be able to read these agent capabilities from your task, but that's also not the case. Some of these are System level and user level Environment Variables, you can get to their values through there, others are supplied by the agent itself and aren't easy to resolve.

Querying the agent version

The Agent will set a Variable you can read from your tasks and a function in the Task Library to ensure you're running on the right agent:

/**
 * Asserts the agent version is at least the specified minimum.
 *
 * @param    minimum    minimum version version - must be 2.104.1 or higher
 */
export function assertAgent(minimum: string): void {
    if (semver.lt(minimum, '2.104.1')) {
        throw new Error('assertAgent() requires the parameter to be 2.104.1 or higher');
    }

    let agent = getVariable('Agent.Version');
    if (agent && semver.lt(agent, minimum)) {
        throw new Error(`Agent version ${minimum} or higher is required`);
    }
}

As you can see in the snippet above, this variable was introduced with agent 2.104.1, detecting it's presence will tell you you're on an older agent, but no on which.

If you'd ask me, this isn't ideal yet.

As task author there is no way to package multiple tasks in the same extension with different supported target platforms. There's no way to ship a version which presents a different UI in 2015 than in 2018 and slowly the number of features available between all the versions of TFS in the field is growing larger.

I hope the Azure DevOps or the Visual Studio Marketplace team will be able to provide us with ways to target specific extension versions to specific TFS versions. That may be the only backwards compatible way that will allow us to keep using the same Extension ID and Task ID in our extension; Which is the only way to not break your users on upgrades.