<img height="1" width="1" style="display:none" src="https://www.facebook.com/tr?id=1678611822423757&amp;ev=PageView&amp;noscript=1">
Defrag This

| Read. Reflect. Reboot.

How to Build an Azure Custom Build/Release Task

Adam Bertram| June 10 2019

| IT insights, PowerShell, Azure, DevOps

Depositphotos_139829526_s-2019

By integrating this REST API in, not only does it show how you can leverage AirTable and Azure, but how you can integrate just about any REST API out there into Azure DevOps.

Creating a Custom Pipeline Task

Azure DevOps, the Microsoft solution providing a comprehensive development environment, has the ability to create custom pipeline tasks. These tasks are built using NodeJS and developed using Typescript, which is a superset language of JavaScript originally developed by Microsoft. Keep in mind that this article, and some code examples within, assume you are using PowerShell for your command line interface.

Here we are going to build a solution to read from a given database within AirTable. We can then take that returned data and pass the information to the next steps in the pipeline. By integrating this REST API in, not only does it show how you can leverage these two tools together, but how you can integrate just about any REST API out there into Azure DevOps.

Prerequisites

NodeJS

Download NodeJS for Windows, which is a JavaScript run-time environment. My personal recommendation is to install the LTS (long-term support) release which is 10.15.3 at time of this writing (though any recent version should work fine). Installing with the defaults will work perfectly fine for this.

https://nodejs.org/en/download/

This package will additionally install NPM (Node Package Manager), which we use to install the other prerequisite packages to develop the task.

After you run through the install, you can verify that it installed properly by running the following at the command line:

node -v
v10.15.3

npm -v
6.9.0

Note: You may need to upgrade NPM if you have it previously installed an older version or would like to run with the latest version.

npm install -g npm@latest

Typescript

Install Typescript, this works only if you have NodeJS and NPM installed already, this will install Typescript globally (via the -g command).

npm install -g typescript

NODE CLI for Azure DevOps (TFX-CLI)

This is a command line tool for interacting with Azure DevOps and Microsoft Team Foundation Server.

npm install -g tfx-cli

Create a Custom Task

  1. Setup the build environment and prerequisites

Create Build and Package Directory


# Create Package Directory

New-Item -Name "buildRelease" -Type Directory

Set-Location buildRelease

Install Packages


# Initialize NPM (node_modules) and Install Prerequisites

npm init -y

npm install azure-pipelines-task-lib --save

npm install typed-rest-client --save

# Both of the items below are for definitions for Node and Q (helper package)

npm install @types/node --save-dev

npm install @types/q --save-dev

Note: We are installing the typed-rest-client because it gives us an easy HTTP client to perform REST API calls within our task.

Setup Typescript


# Make sure we don't commit the node_modules and it's many thousands of files

echo node_modules > .gitignore

# Initialize Typescript

tsc --init

# Change Typescript Target Language to ES6 from ES5. ES6 is the newest iteration of the Javascript language syntax and functionality and has many benefits for ease of use

((Get-Content -Path '.\tsconfig.json' -Raw) -replace '"es5"','"es6"') | Set-Content -Path '.\tsconfig.json'

2. Create the task .json file in our buildRelease working folder via the below script:

This defines our task, the entry point and also what inputs we require. In our case, we are going to define an AirTable task that we capture the API key and the base URL for a REST call.

# Only required to have a unqiue GUID

$GUID = (New-Guid).Guid

$Name         = "get-airtable-data"

$FriendlyName = "Retrieve AirTable Data"

$Description  = "This is a task to retrieve Airtable Data."

$Author       = "Adam Bertram"

$JSON = @"

{

    "id": "${GUID}",

    "name": "${Name}",

    "friendlyName": "${FriendlyName}",

    "description": "${Description}",

    "helpMarkDown": "",

    "category": "Utility",

    "author": "${Author}",

    "version": {

        "Major": 0,

        "Minor": 1,

        "Patch": 0

    },

        "visibility": [

        "Build",

        "Release"

    ],

    "instanceNameFormat": "Retrieving $(baseurl) from AirTable",

    "inputs": [

        {

            "name": "baseurl",

            "type": "string",

            "label": "Base URL",

            "defaultValue": "",

            "required": true,

            "helpMarkDown": "The base URL for the REST API call within AirTable"

        },

                {

            "name": "apikey",

            "type": "string",

            "label": "API Key",

            "inputMode": "passwordbox",

            "isConfidential": true,           

            "defaultValue": "",

            "required": true,

            "helpMarkDown": "AirTable API Key"

        }

    ],

    "execution": {

        "Node": {

            "target": "index.js"

        }

    }

}

"@
$JSON | Set-Content -Path "./task.json"

3. Create the ts file that actually contains the logic of our new task. For this AirTable task, we will save the JSON output of the results to a variable that can be passed on to the next task.

$boilerplate = @"
import tl = require('azure-pipelines-task-lib/task');
import httpc = require('typed-rest-client/HttpClient');

async function run() {
try {
        const baseURL: string = tl.getInput('baseurl', true);
        const APIKey: string = tl.getInput('apikey', true);

        if (baseURL == 'bad') {
            setResult(tl.TaskResult.Failed, 'Bad input was given');
            return;
        }

        if (APIKey == 'bad') {
            tl.setResult(tl.TaskResult.Failed, 'Bad input was given');
            return;
        }

        console.log(baseURL);
        console.log(APIKey);

        let httpClient: httpc.HttpClient = new httpc.HttpClient('Test', []);

        let result = await httpClient.get(baseURL, {
            'Accept': 'application/json',
            'Authorization': 'Bearer ' + APIKey
        });

        let body: string = await result.readBody();
        let obj:any = JSON.parse(body);

        console.log(obj.records);

        tl.setVariable("AIRTABLERESULT", body, false);
    }
    catch (err) {
        tl.setResult(tl.TaskResult.Failed, err.message);
    }
}

run();
"@

$boilerplate | Set-Content -Path "./index.ts"

4. Compile the Typescript file in JavaScript for deployment. This command takes our ts file and compiles it to js. This will also output any obvious errors upon compiling. 

tsc

Verify the Task

We set environmental variables as that is how data is passed around within the build environments. Upon running our compiled index.js file, it will read in the environmental variables and output the results.

$env:INPUT_APIKEY="MyAPIKey"
$env:INPUT_BASEURL="https://api.airtable.com/v0/asd8f88ff8ads/Test"

node index.js

Example Results: 

##vso[task.debug]agent.TempDirectory=undefined
##vso[task.debug]agent.workFolder=undefined
##vso[task.debug]loading inputs and endpoints
##vso[task.debug]loading INPUT_APIKEY
##vso[task.debug]loading INPUT_BASEURL
##vso[task.debug]loaded 2
##vso[task.debug]Agent.ProxyUrl=undefined
##vso[task.debug]Agent.CAInfo=undefined
##vso[task.debug]Agent.ClientCert=undefined
##vso[task.debug]Agent.SkipCertValidation=undefined
##vso[task.debug]baseurl=https://api.airtable.com/v0/asd8f88ff8ads/Test
##vso[task.debug]apikey=key343434343
https://api.airtable.com/v0/asd8f88ff8ads/Test
key343434343
[ { id: 'recLpzf2VZ6jyGIbm',
    fields: { Name: 'Record 2', Value: 'Value 2' },
    createdTime: '2019-04-25T04:16:25.000Z' },
  { id: 'recP6d01urYlVQOS5',
    fields: { Name: 'Record 1', Value: 'Value 1' },
    createdTime: '2019-04-25T04:16:25.000Z' },
  { id: 'recPyPXYbvTScUFn1',
    fields: { Name: 'Record 3', Value: 'Value 3' },
    createdTime: '2019-04-25T04:16:25.000Z' } ]
##vso[task.debug]set AIRTABLERESULT={"records":[{"id":"recLpzf2VZ6jyGIbm","fields":{"Name":"Record 2","Value":"Value 2"},"createdTime":"2019-04-25T04:16:25.000Z"},{"id":"recP6d01urYlVQOS5","fields":{"Name":"Record 1","Value":"Value 1"},"createdTime":"2019-04-25T04:16:25.000Z"},{"id":"recPyPXYbvTScUFn1","fields":{"Name":"Record 3","Value":"Value 3"},"createdTime":"2019-04-25T04:16:25.000Z"}]}
##vso[task.setvariable variable=AIRTABLERESULT;issecret=false;]{"records":[{"id":"recLpzf2VZ6jyGIbm","fields":{"Name":"Record 2","Value":"Value 2"},"createdTime":"2019-04-25T04:16:25.000Z"},{"id":"recP6d01urYlVQOS5","fields":{"Name":"Record 1","Value":"Value 1"},"createdTime":"2019-04-25T04:16:25.000Z"},{"id":"recPyPXYbvTScUFn1","fields":{"Name":"Record 3","Value":"Value 3"},"createdTime":"2019-04-25T04:16:25.000Z"}]}

Package and Publish the Task

To publish this extension we need to define our manifest file taht helps to define where and in what cases our extension will show up. 

Manifest Documentation

https://docs.microsoft.com/en-us/azure/devops/extend/develop/manifest?view=azure-devops

Key Notes

  • The publisher must match the publisher you create within Azure DevOps marketplace
  • Files → Path must point to our build directory
  • Contributions → Properties → Name must also point to our build directory
  • The vss-extension.json file lives in the root directory, one up from the build directory
$Publisher = "adam-bertram" $PublisherFriendly = "Adam Bertram" $Description = "Build and Release Tools"
$Manifest = @" { "manifestVersion": 1,  "id": "build-release-task",  "name": "${PublisherFriendly} Build and Release
Tools", "version": "0.0.1", "publisher": "$Publisher",  "targets": [  {  "id": "Microsoft.VisualStudio.Services"  }  ],
"description": "${Description}", "categories": [ "Azure Pipelines", "Build and release" ], "tags": [ "release", "build" ],
"files": [ { "path": "buildRelease" } ], "contributions": [ { "id": "custom-build-release-task", "type": "ms.vss-distributed-
task.task", "targets": [ "ms.vss-distributed-task.tasks" ], "properties": { "name": "buildRelease" } } ] } "@

Must be in root

Set-Location .. $Manifest | Set-Content -Path "./vss-extension.json"

Package the Extension

This will package up the extension into a .vsix file that we can use to upload to the Marketplace.

Note: Every time we need to package a version, it's required we rev the version. It's easiest to add the --rev-version parameter so that we automatically do so every time.

tfx extension create --manifest-globs vss-extension.json --rev-version

Create a Publisher

It's required that extensions are identified from a provider, Microsoft's included. If you haven't created one yet, do so with the following steps.

  1. Sign in to the Visual Studio Marketplace Publishing Portal
  2. Enter in the Provider details:
    • Create an identifier for your publisher: mycompany-myteam
    • Specify a display name: My Team
  3. Click Create

Upload Your Extension

Once a publisher has been created, you can now upload your packaged extension. To do so, click on New extension → Azure DevOps.

You will be prompted to either Drag and Drop or upload the .vsix file that was created earlier by packaging our new extension.

Share and Install Your Extension

To allow an organization to use your new extension, share it with one or more so that you may install and test your extension.

  1. Right-click on your extension and select Share, enter in your organization identifier
  2. Navigate to your organization within Azure DevOps, click the Marketplace icon in the upper right corner and click on Manage Extensions
  3. Click on the name of the Extension to navigate to it in the Marketplace, once there click on the Get it free button.
  4. Select the organization from the drop-down menu and click on Install

After the extension is installed, you will be able to find this task within the Build and Release tasks list. You can add one, test the inputs and see the output within the log files.

Topics: IT insights, PowerShell, Azure, DevOps

Leave a Reply

Your email address will not be published. Required fields are marked *

THIS POST WAS WRITTEN BY Adam Bertram

Adam Bertram is a 20-year veteran of IT. He’s currently an automation engineer, blogger, independent consultant, freelance writer, author, and trainer. Adam focuses on DevOps, system management, and automation technologies as well as various cloud platforms. He is a Microsoft Cloud and Datacenter Management MVP and efficiency nerd that enjoys teaching others a better way to leverage automation.

Free Trials

Getting started has never been easier. Download a trial today.

Download Free Trials

Contact Us

Let us know how we can help you. Focus on what matters. 

Send us a note

Subscribe to our Blog

Let’s stay in touch! Register to receive our blog updates.