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:
- Building Azure DevOps Extension - Design
- Building Azure DevOps Extension - Implementation
- Building Azure DevOps Extension - Publisher Registration
- Building Azure DevOps Extension - Manual Publish
- Building Azure DevOps Extension - Automated Publish 1
- 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 |
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 |
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.
install
Task
Implementing 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:
- To import all necessary modules,
- To declare the
run()
function, and - 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); | |
} |
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()
andexec()
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.
deploy
Task
Implementing 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); | |
} |
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 |
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 |
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.