8 min read

Building Azure DevOps Extension on Azure DevOps - Implementation

Justin Yoo

In my previous post, we completed designing Azure DevOps extension. This post continues implementing the extension from there.

Table of Contents

This series consists of those six posts:

  1. Building Azure DevOps Extension - Design
  2. Building Azure DevOps Extension - Implementation
  3. Building Azure DevOps Extension - Publisher Registration
  4. Building Azure DevOps Extension - Manual Publish
  5. Building Azure DevOps Extension - Automated Publish 1
  6. Building Azure DevOps Extension - Automated Publish 2

Use Case Scenario

I'm interested in using a static website generator, called Hugo, to publish a website. There's an extension already published in the marketplace so that I'm able to install it for my Azure DevOps organisation. To publish this static website, I wanted to use Netlify. However, it doesn't yet exist, unfortunately. Therefore, I'm going to build an extension for Netlify and at the end of this series, you will be able to write an extension like what I did.

Actually, this Netlify extension has already been published, which you can use it straight away. This series of posts is a sort of reflection that I fell into situations – some are from the official documents but the others are not, but very important to know during the development. The source code of this extension can be found at this GitHub repository.

Implementing Azure DevOps Extension

Based on the design from my previous post, the task.json invokes the index.js file. Therefore, I'm going to write it.

In addition to this, TypeScript is required for this implementation. If you don't have it yet, install it using the command below:

npm install -g typescript

Scaffolding Task Structure

Each task uses node.js, as defined in task.json. As both install and deploy need node.js modules, create the package.json file under the src folder. You can create the package.json file individually for each task if you like.

npm init
view raw npm-init.cmd hosted with ❤ by GitHub

Once package.json is created, run the following commands to install azure-pipelines-task-lib and type definitions.

npm install azure-pipelines-task-lib --save
npm install @types/node --save-dev
npm install @types/q --save-dev

Finally, create tsconfig.json by running the following command:

tsc --init
view raw tsc-init.cmd hosted with ❤ by GitHub

Once created, open it and update the compile option from ES5 to ES6. Now we've got all scaffolding done for implementation. Your files and folder structure might look like this with package.json, package-lock.json, tsconfig.json and node_modules:

Don't worry about the content in package.json for now, because it's not relevant to our extension itself. We don't even publish this to npm, anyway.

Implementing install Task

It's time for the implementation! First of all, create the index.ts file under the install folder and enter the following code:

import * as path from 'path';
import * as tl from 'azure-pipelines-task-lib/task';
async function run() {
}
run();

This is the easiest and simplest boilerplate template. Of course, you can create a more sophisticated one, but this is beyond our topic for now. What it describes is:

  1. To import all necessary modules,
  2. To declare the run() function, and
  3. To call the run() function.

All we need to do is just to put all logics into the run() function. Let's write the logic. Enter the following snippet into the run() function.

tl.setResourcePath(path.join(__dirname, 'task.json'));
try {
const version: string = tl.getInput('version', false);
const args: Array<string> = new Array<string>();
args.push('install');
args.push('-g');
if (version) {
args.push('netlify-cli@' + version);
}
else {
args.push('netlify-cli');
}
await tl.exec('npm', args);
}
catch (err) {
tl.setResult(tl.TaskResult.Failed, err.message);
}
view raw install.ts hosted with ❤ by GitHub

The purpose of this task is to install netlify-cli in the pipeline so that the next task can run it. More specifically, this task runs the following command through this index.js file.

npm install -g netlify-cli@[version]

As the version number is the only information we need to know for this task, we got the version value from task.json, according to my previous post. This version number is passed through the function, getInput('version', false). The first argument, version is the input field name from task.json. Both value MUST be the same as each other in both index.ts and task.json; otherwise, it will throw an error.

At the last line in the try block, it invokes the function, exec('npm', args), which installs the netlify-cli npm package to the pipeline. The second argument value, args is an array created a few lines above the exec() function.

This task only uses getInput() and exec() functions, but there are many other functions worth having a look. This document is the official reference.

And finally, if there's any error occurs, the entire process stops and throws the error back to the pipeline so that the pipeline itself can handle this error, by using the try...catch block.

Implementing deploy Task

This time, let's implement the deploy task. The overall process is the same as the install task writing. First of all, create the index.ts file under the deploy folder and enter the boilerplate code in it. Then fill up the run() function with the following:

tl.setResourcePath(path.join(__dirname, 'task.json'));
try {
const authToken: string = tl.getInput('authToken', true);
const siteId: string = tl.getInput('siteId', true);
const sourceDirectory: string = tl.getPathInput('sourceDirectory', true, true);
const isValidationOnly: boolean = tl.getBoolInput('isValidationOnly', true);
const message: string = tl.getInput('message', false);
const functionsDirectory: string = tl.getPathInput('functionsDirectory', false, true);
const args: Array<string> = new Array<string>();
args.push('deploy')
args.push('--auth=' + authToken);
args.push('--site=' + siteId);
args.push('--dir=' + sourceDirectory);
if (!isValidationOnly) {
args.push('--prod');
}
if (message) {
args.push('--message=' + message);
}
if (functionsDirectory) {
args.push('--functions=' + functionsDirectory);
}
args.push('--json');
await tl.exec('netlify', args);
}
catch (err) {
tl.setResult(tl.TaskResult.Failed, err.message);
}
view raw deploy.ts hosted with ❤ by GitHub

This task will eventually run the command:

netlify deploy --auth=*** --site=*** --dir=*** --prod --functions=*** --json

Therefdore, the task.json receives many values from the UI and they will be used in this function. You can focus a few functions here in this task snippet – getInput() for string input, getPathInput() for path input, and getBoolInput() for boolean input.

Testing on Local Machine

All implementations have completed! Now it's time for testing the extension locally. I'm not covering unit test bits and pieces here in this post, but covering to confirm whether the extension works or not. Run the following command at the src folder, as there is tsconfig.json.

tsc
view raw tsc-compile.cmd hosted with ❤ by GitHub

Now all .ts files have been compiled to .js files. Check whether both install/index.js and deploy.index.js exist. By the way, we need to pass the version value to the install task. But the run() function doesn't accept any parameter. How can we pass the value, then? As the getInput() function passes the value, we need to prepare the value for it to identify. According to the official document, those values are set to the environment variables. Therefore run the following command, depending on your platform.

# PowerShell
$env:INPUT_VERSION = "2.11.23"
# bash
export INPUT_VERSION="2.11.23"

Once it's set, run the following command to run the install task.

node install/index.js
view raw node-run.cmd hosted with ❤ by GitHub

Now we can confirm that the install task has been run in your local machine. Likewise, deploy/index.js can be run in the same way.


So far, we've implemented the Azure DevOps extension. Once everything is done, the folder structure might look like:

As we complete development, we need to publish this extension to Marketplace. Let's discuss how to register a publisher for the extension in the next post.

More Readings