
Since I upgraded my team’s private TFS instance to TFS 2015 RC1, followed by RC2, the whole team has been working with TFS 2015 quite a lot. Of course one of the major features is the new build engine and we’ve given that quite a ride. From cross platform builds on Mac and Linux to custom build tasks, we’ve accomplished quite a lot. Seeing as during yesterday’s Visual Studio 2015 launch, Brian Harry stated that it was ‘quite easy’ to build your own tasks, I figured I’d give a short write-down of our experiences with custom tasks.
Preface
From the moment I upgraded our R&D server to RC1, we’ve been working with the new build system. Up until RC2 it was only possible to add custom build tasks, but we weren’t able to remove them. On top of that, the whole process isn’t documented quite yet. Seeing as we quite often add NuGet packages to a feed and didn’t want to add a, not very descriptive, PowerShell task to all of our build definitions, we decided to use this example for a custom task and see how it would fare.
Prerequisite one: What is a task?
To make a custom build task, we first need to know what it looks like. Luckily Microsoft has open-sourced most of the current build tasks in https://github.com/Microsoft/vso-agent-tasks which gave us a fair idea of what a build task is:
- a JSON file describing the plugin
- a PowerShell or Node.JS file containing the functionality (this post will focus on PowerShell)
- an (optional) icon file
- optional resources translating the options to another language
Now the only thing we needed to find out was: how to upload these tasks and in what format?
Good to know:
- To make sure your icon displays correctly, it must be 32×32 pixels
- The task ID is a GUID which you need to create yourself
- The task category should be an existing category
- Visibility tells you what kind of task it is, possible values are: Build, Release and Preview. Currently only Build-type tasks are shown.
Prerequisite two: How to upload a task?
We quickly figured out that the tasks were simply .zip files containing the aforementioned items, so creating a zip was an easy but then we needed to get it there. By going through the github repository’s, we figured out there was a REST-API which controls all the tasks and we figured that by doing a PUT-call to said endpoint we could create a new task, but also overwrite tasks.
The following powershell-script enables you to upload tasks:
param( [Parameter(Mandatory=$true)][string]$TaskPath, [Parameter(Mandatory=$true)][string]$TfsUrl, [PSCredential]$Credential = (Get-Credential), [switch]$Overwrite = $false ) # Load task definition from the JSON file $taskDefinition = (Get-Content $taskPathtask.json) -join "`n" | ConvertFrom-Json $taskFolder = Get-Item $TaskPath # Zip the task content Write-Output "Zipping task content" $taskZip = ("{0}..{1}.zip" -f $taskFolder, $taskDefinition.id) if (Test-Path $taskZip) { Remove-Item $taskZip } Add-Type -AssemblyName "System.IO.Compression.FileSystem" [IO.Compression.ZipFile]::CreateFromDirectory($taskFolder, $taskZip) # Prepare to upload the task Write-Output "Uploading task content" $headers = @{ "Accept" = "application/json; api-version=2.0-preview"; "X-TFS-FedAuthRedirect" = "Suppress" } $taskZipItem = Get-Item $taskZip $headers.Add("Content-Range", "bytes 0-$($taskZipItem.Length - 1)/$($taskZipItem.Length)") $url = ("{0}/_apis/distributedtask/tasks/{1}" -f $TfsUrl, $taskDefinition.id) if ($Overwrite) { $url += "?overwrite=true" } # Actually upload it Invoke-RestMethod -Uri $url -Credential $Credential -Headers $headers -ContentType application/octet-stream -Method Put -InFile $taskZipItem
Good to know:
- Currently only ‘Agent Pool Administrators’ are able to add/update or remove tasks.
- Tasks are server-wide, this means that you will upload to the server, not to a specific collection or project.
Creating the actual task
So like I said, we’ll be creating a new task that’s going to publish our NuGet packages to a feed. So first we need to decide what information we need to push our packages:
- The target we want to pack (.csproj or .nuspec file relative to the source-directory)
- The package source we want to push to
For this example I’m assuming you’re only building for a single build configuration and single target platform, which we’ll use in the PowerShell-script.
First we’ll make the task definition. As I said, this is simply a JSON file describing the task and its inputs.
{ "id": "61ed0e1d-efb7-406e-a42b-80f5d22e6d54", "name": "NuGetPackAndPush", "friendlyName": "Nuget Pack and Push", "description": "Packs your output as NuGet package and pushes it to the specified source.", "category": "Package", "author": "Info Support", "version": { "Major": 0, "Minor": 1, "Patch": 0 }, "minimumAgentVersion": "1.83.0", "inputs": [ { "name": "packtarget", "type": "string", "label": "Pack target", "defaultValue": "", "required": true, "helpMarkDown": "Relative path to .csproj or .nuspec file to pack." }, { "name": "packagesource", "type": "string", "label": "Package Source", "defaultValue": "", "required": true, "helpMarkDown": "The source we want to push the package to" } ], "instanceNameFormat": "Nuget Pack and Push $(packtarget)", "execution": { "PowerShell": { "target": "$(currentDirectory)\PackAndPush.ps1", "argumentFormat": "", "workingDirectory": "$(currentDirectory)" } } }
This version of the task will be a very rudimentary one, which doesn’t do much (any) validation, so you might want to add that yourself.
[cmdletbinding()] param ( [Parameter(Mandatory=$true)][string] $packtarget, [Parameter(Mandatory=$false)][string] $packagesource ) #################################################################################################### # 1 Auto Configuration #################################################################################################### # Stop the script on error $ErrorActionPreference = "Stop" # Relative location of nuget.exe to build agent home directory $nugetExecutableRelativePath = "AgentWorkerToolsnuget.exe" # These variables are provided by TFS $buildAgentHomeDirectory = $env:AGENT_HOMEDIRECTORY $buildSourcesDirectory = $Env:BUILD_SOURCESDIRECTORY $buildStagingDirectory = $Env:BUILD_STAGINGDIRECTORY $buildPlatform = $Env:BUILDPLATFORM $buildConfiguration = $Env:BUILDCONFIGURATION $packagesOutputDirectory = $buildStagingDirectory # Determine full path of pack target file $packTargetFullPath = Join-Path -Path $buildSourcesDirectory -ChildPath $packTarget # Determine full path to nuget.exe $nugetExecutableFullPath = Join-Path -Path $buildAgentHomeDirectory -ChildPath $nugetExecutableRelativePath #################################################################################################### # 2 Create package #################################################################################################### Write-Host "2. Creating NuGet package" $packCommand = ("pack `"{0}`" -OutputDirectory `"{1}`" -NonInteractive -Symbols" -f $packTargetFullPath, $packagesOutputDirectory) if($packTargetFullPath.ToLower().EndsWith(".csproj")) { $packCommand += " -IncludeReferencedProjects" # Remove spaces from build platform, so 'Any CPU' becomes 'AnyCPU' $packCommand += (" -Properties `"Configuration={0};Platform={1}`"" -f $buildConfiguration, ($buildPlatform -replace 's','')) } Write-Host ("`tPack command: {0}" -f $packCommand) Write-Host ("`tCreating package...") $packOutput = Invoke-Expression "&'$nugetExecutableFullPath' $packCommand" | Out-String Write-Host ("`tPackage successfully created:") $generatedPackageFullPath = [regex]::match($packOutput,"Successfully created package '(.+(?<!.symbols).nupkg)'").Groups[1].Value Write-Host `t`t$generatedPackageFullPath Write-Host ("`tNote: The created package will be available in the drop location.") Write-Host "`tOutput from NuGet.exe:" Write-Host ("`t`t$packOutput" -Replace "`r`n", "`r`n`t`t") #################################################################################################### # 3 Publish package #################################################################################################### Write-Host "3. Publish package" $pushCommand = "push `"{0}`" -Source `"{1}`" -NonInteractive" Write-Host ("`tPush package '{0}' to '{1}'." -f (Split-Path $generatedPackageFullPath -Leaf), $packagesource) $regularPackagePushCommand = ($pushCommand -f $generatedPackageFullPath, $packagesource) Write-Host ("`tPush command: {0}" -f $regularPackagePushCommand) Write-Host "`tPushing..." $pushOutput = Invoke-Expression "&'$nugetExecutableFullPath' $regularPackagePushCommand" | Out-String Write-Host "`tSuccess. Package pushed to source." Write-Host "`tOutput from NuGet.exe:" Write-Host ("`t`t$pushOutput" -Replace "`r`n", "`r`n`t`t")
To finish up, don’t forget to add a .png logo to your task 😉
You should now be able to add a custom task to your build pipeline from the “Package” category:
Words of warning
Tasks can be versioned, use this to your advantage. All build definitions use the latest available version of a specific task, you can’t change this behavior from the web interface, so always assume the latest version is being used.
If you don’t change the version number of your task when updating it, the build agents that have previously used your task will not download the newer version because the version number is still the same. This means that if you change the behavior of your task, you should always update the version number!
When deleting a task, this task is not automatically removed from current build definitions, on top of that you won’t get a notification when editing the build definition but you will get an exception on executing a build based on that definition.
Tasks are always available for the entire TFS instance, this means that you shouldn’t include credentials or anything that you don’t want others to see. Use ‘secret variables’ for this purpose:
Further reading/watching
If you’ve followed this post so far, I recommend you also check out my team member Jonathan’s post/videos (in Dutch) out:
Blog Post about Invoke SQLCmd in build vNext
Video on build vNext (in Dutch)
21 comments
Thanks a lot for this post Peter! It was the missing piece in our TFS build workflow.
Do you know what kind of permissions the user needs to have to push those tasks? Only our TFS administrator is allowed and we’d like to share the rights without giving administration permissions. We have this error message:
“Access denied. {userLogin} needs Manage permissions to perform the action. For more information, contact the Team Foundation Server administrator.”
jdebarochez
You’re quite welcome, glad it helped. You have to be in the ‘agent pool administrators’ group, or TF admin to be able to upload tasks.
Peter Toonen
I love this article! This is exactly what I needed.
Couple of questions.
1. Does every TFS 2015 server have the “_apis/distributedtask/tasks/”, as used in your upload powershell script? I try to hit http://mytfsserver/_apis/distributedtask/tasks with a web browser, and I get nothing. I will check IIS for that folder, but I wonder if every on-premise TFS 2015 server should have this?
2. What do you think about using (zipping up) the build tasks made by Microsoft for VSO, instead of creating my own like your article has. (https://github.com/Microsoft/vso-agent-tasks/tree/master/Tasks/NugetPackager and https://github.com/Microsoft/vso-agent-tasks/tree/master/Tasks/NugetPublisher).
Thanks for this well written and detailed article!
Steve K.
Steve Kennedy
Hi Steve,
To answer your questions:
1) Yes, every TFS 2015 server should have those files, mind you that they’re under the /tfs virtual application/directory. So it would be something like http://yourtfsserver:8080/tfs/_apis/distributedtask/tasks
2) At the time I wrote this article those tasks were not yet available and I used NuGet packaging as an example, I would of course recommend to use the Microsoft supplied tasks. These two tasks will be in TFS from update 1 on (install and play with the RC if you haven’t yet).
3) You should have that folder, but as I said under 2, it will be under the /tfs virtual directory.
On another note, since writing this article, a new utility has come into play: https://github.com/Microsoft/tfs-cli You can use this utility to easily upload tasks to your server.
Thanks,
Peter
Peter Toonen
Thank you so much for your answers! I really appreciate it. With your help, I finally was able to get everything working. Thanks again!
Steve Kennedy
Eek.
3. If I don’t have the “_apis” folder on my TFS server, is there another way to upload the task to the server? Meaning, are these tasks stored in the local file system somewhere on my TFS server?
Steve Kennedy
A very good article, worked perfect to me. Just one question.
If I like to import a .net Assembly in powershell, where can I place this exclusive assemblies? Or isn’t that an option?
Thanks,
Ralf
Ralf
Thanks! If it’s a non-default assemblies, you have several options:
1) Install it onto every build server (using a SDK for example) you will use. This may work well if you have control over all build servers, but might not be your best solution.
2) Place the assembly in some sort of artifact repository, where your task will download it from whilst running and then discard it after the build.
3) Include it into your task. Take a look at the SonarQube pre-build task for an example:
https://github.com/Microsoft/vso-agent-tasks/tree/master/Tasks/SonarQubePreBuild/MSBuild.SonarQube.Runner-1.0.1
My personal preference would be 2. That keeps the task light-weight and allows you to update the assemblies without updating the task – providing the assembly’s public API doesn’t change of course.
Peter
Peter Toonen
I am using the TFS 2015 with update 1 ISO to install on a Windows 2012 R2 server with SQL Server 2014. When I launch the install process and select “Full Server”, the “Build” item in the left hand list, which is supposed to appear under the “Application Tier” item, is missing.
What is happening ?
Joshua
In TFS 2015 the build has drastically changed: https://msdn.microsoft.com/Library/vs/alm/Build/overview
This also means that in the installer, the ‘build’ option has been moved to ‘Additional Tools and Components’. The (legacy) XAML build is now ‘Configure XAML Build Service’. New (tfs 2015) build-agents can be installed after the installation.
Peter Toonen
Thank you a lot for a knowledge sharing. Your upload script works perfectly
Could you please advise how to delete created tasks using api request?
Yegor
Thanks Yegor,
you can use the following powershell script to uninstall.
param(
[Parameter(Mandatory=$true)][string]$TaskPath,
[Parameter(Mandatory=$true)][string]$TfsUrl,
[PSCredential]$Credential = (Get-Credential)
)
# Gather the necessary information
$taskDefinition = (Get-Content $taskPathtask.json) -join "`n" | ConvertFrom-Json
$headers = @{ "Accept" = "application/json; api-version=2.0-preview"; "X-TFS-FedAuthRedirect" = "Suppress" }
$url = "$TfsUrl/_apis/distributedTask/tasks/$($taskDefinition.id)"
# Perform the actual request
Invoke-RestMethod -Uri $url -Credential $credential -Headers $headers -Method Delete
The parameter ‘TaskPath’ refers to the directory which contains the unzipped task definition.
– Peter
Peter Toonen
Great post. Created a custom task, it is showing in the database but not visible in the builds. what could be the wrong?
4SB
First thing that pops to mind is that the task category is incorrect (non-existing). If that’s the case, just re-upload.
– Peter
Peter Toonen
It shows up now.. not sure how and why.
4SB
[…] P.S. ????????? ?????????? ? ???????? ??????????? build-task ? ???????? ?? ? TFS ????? ????? ?? ??????: https://blogs.infosupport.com/custom-build-tasks-in-tfs-2015/ […]
????????? ??????? ?????? ? email-??????????? ?? TFS 2015 (scripted builds) | ???? ???????? Rubius
[…] followed closely the design guidance found here, here and here, but I keep getting this PowerShell […]
TFS 2015.3 custom build step not sending variables to the script | Codeba
Thanks
I cant see how the task is “published” into the TFS server ?
What am I missing or cant see ?
regards
Greg Roberts
hi my custom task is not picking any agent
Sandeep
Hi I am getting this error:
VSTS task module not found at expected location: C:TfsDataAgentsAgent-customtasktasksSandeepTask .1.0ps_modulesVstsTaskSdkVstsTaskSdk.psd1
Sandeep
please help me on creating new category.
Sandeep