Last Updated: 9 September 2022

Custom Powershell C# Azure DevOps build task

The build pipeline can be greatly enhanced and customized to your organization's needs. Your custom tasks can be as simple as a few lines of Powershell or Bash script, or more complicated by creating your own command line utility using .Net Core or .Net 6.

The aim of creating custom build tasks is modularity and re-usability. Breaking up your pipeline like this makes things less fragile, too.

This demo will show you how to create a custom build task, which uses a Powershell script to call a custom command line utility written in C# & .Net 6. The process is broadly the same even if you choose to use something else to create your command line tool, such as Java. Calling out to a CLI utility gives us great flexibility, but if you can achieve what you need purely with Powershell, then it isn't necessary.

Looking to create a cross-platform custom build task using node.js instead? Check out our supplementary guide here!

Step 1 - Create folder structure

Create the following folder structure:

Custom task folder structure

Step 2 - Task Definition

The next thing to do is to create the package definition metadata file task.json, which goes in the 'Task' folder. For more info about task.json, check out the task.json schema on Microsoft's GitHub. For advanced tasks, you can group parameters together, specify rules for when parameters are visible to the user, and more.


{
    "$schema": "https://raw.githubusercontent.com/Microsoft/azure-pipelines-task-lib/master/tasks.schema.json",
    "id": "YOUR-GENERATED-GUID-HERE",
    "name": "MyCustomTask",
    "friendlyName": "Demo Task",
    "description": "This is a demo task.",
	"helpMarkDown": "For more information about this task, visit www.example.com",
    "category": "Azure Pipelines",
    "author": "Joe Bloggs",
    "version": {
        "Major": 1,
        "Minor": 0,
        "Patch": 0
    },
    "instanceNameFormat": "MyCustomTask",
    "inputs": [
		{
            "name": "someString",
            "type": "string",
            "label": "Some string",
            "required": true,
            "helpMarkDown": "This is an example string input"
		},
		{
            "name": "anotherString",
            "type": "string",
            "label": "Another string",
            "required": true,
            "helpMarkDown": "This is another example",
            "defaultValue": "Hello World"
		},
	],
    "execution": {
        "Powershell": {
            "target": "${CurrentDirectory}\\MyCustomTask.ps1",
            "argumentFormat": "",
            "workingDirectory": ""
        }
    }
}

  • Id - Add a unique GUID using guidgenerator.com
  • Give the task a name, friendly name, description and author
  • Category decides where your task will appear in Azure DevOps, e.g. 'Azure Pipelines' makes it available to your build and release pipelines. There are some other categories for use with legacy Team Foundation Server (TFS) installations, see the task.json schema for further information on this.
  • Version Builds will always use the latest version of your task within the same major version. So if your build uses v1.0.0 and you create a v1.1.0, the build agents will automatically start using v1.1.0. They will not use v2.0.0 unless you explicitly go into the build UI and change the version from the drop down, or specify the major version in the task's YAML, e.g. MyCustomTask@1 for version 1.x.
  • InstanceNameFormat will be used as the task title whenever you add it to a build
  • Inputs - The is where you can define various variables to pass to your script. You can use string, boolean, multiline, filePath, pickList or radio (others are available but these are the most common ones).
  • Execution - this section tells what script to run when the task is executed. You can use $(currentDirectory) to execute a script in the task's root folder.

You can also add a 32 x 32 png icon in the root of your task folder, which should be named icon.png.

Step 3 - Create The Powershell Script

Here is a complete example of a Powershell script that calls the custom CLI tool we are going to write, with the 'someString' and 'anotherString' value passed in as parameters. Put this script in the 'Task' folder, with the same filename that you set in the 'Execution' property of your task.json file.


    param(
	    [string]$someString,
	    [string]$anotherString	
    )

    function Get-ExePath
    {
        $currentDir = (Get-Item -Path ".\").FullName

        $exeDir = Join-Path $currentDir "MyCustomTask"
        $exePath = Join-Path $exeDir "MyCustomTask.exe"
	
        return $exePath
    }

    $exePath = Get-ExePath

    & "$exePath" /someString="$someString" /anotherString="$anotherString"

    if ($LastExitCode -ne 0)
    {
	    Write-Error "Task failed."
	    exit 1
    }

Step 4 - Create Command Line Tool using C#

We are going to create a custom command line tool, which is called by the Powershell script above. The Visual Studio solution file goes in the Command folder, with the rest of the source code and .csproj file in the MyCustomTaskCLI folder underneath. In truth, it does not really matter how you structure your C# source code folders, the key thing is copying the build output from the /bin folder into your task folder structure.

Create a new .Net 6 Console App. You can read the arguments passed in from the Powershell script by parsing the 'args' collection passed into the Main() method. You also need to make sure you exit properly using Environment.Exit(), so that Azure DevOps can identify if your CLI tool was successful, or encountered an error.



        static void Main(string[] args)
        {
            foreach(var arg in args)
            {
                Console.WriteLine(arg);
            }

            Environment.Exit(0);        // Exit with success
            // Environment.Exit(1);        // Exit with failure
        }

Anything you write to the Console window will end up in the pipeline build logs.

When you have finished writing your custom CLI tool in C#, build it in 'Release' mode, and copy the contents of the /bin/Release folder to MyCustomTask -> Task -> MyCustomTask folder. This will ensure that all the paths are correct and that the Powershell bootstrapping script can call it.

Step 5 - Upload to Azure DevOps

First install the TFX CLI tool, which we will use for interacting with the Azure DevOps server. This requires Node so if you don't have it installed, download and install the latest version from nodejs.org. This also installs npm by default. To install the TFX CLI tool:



        npm install -g tfx-cli

TFX needs to be able to authenticate with your Azure DevOps instance. To do this, we will create a Personal Access Token (PAT). Log into Azure DevOps and click the person icon in the top-right corner, and then select 'Personal Access Tokens'. Create a new token with full permissions. From Powershell run:



        tfx login --authType pat

Enter your Azure DevOps url, e.g. dev.azure.com/your-organization and the token you just created. You should receive a message saying you have logged in successfully. To upload your task, run the following, where the path is the location of your task.json file:



        tfx build tasks upload --task-path C:\temp\MyCustomTask\Task

This should be successful, and to confirm you can run:



        tfx build tasks list

Another way to confirm that your new task is available to your pipelines, is to go into the Azure DevOps web app and edit any pipeline YAML. You should be able to search for your new task using the filter on the right hand side.

Custom task available in Azure DevOps web app

Step 6 - Delete Custom Task from Azure DevOps

To delete a custom task, run the following, remembering to use the correct GUID that you specified originally in your task.json file. See above for instructions on how to authenticate the TFX tool against your Azure DevOps instance.



        tfx build tasks delete --task-id b9780285-58e8-48c4-8281-4267ddc9ac90