Developing Azure DevOps Extension
Introduction
Azure DevOps Services provides a lot of space for importing your custom extension. Work items, pipeline tasks, dashboard widgets, and other options can serve this purpose.
This article will be about developing extensions for pipeline tasks — I will describe how to create the easiest sample extension that will help organize your own continuous integration.
But first, let’s describe the whole process of hosting the extension on Azure DevOps and how it works. Then we will discuss its detailed development route.
General Azure DevOps hosting and working scheme
On the picture down below, you can see the general Azure DevOps hosting and working scheme for pipelines.
As you can see in the picture, the extension is a number of tasks. Each task can be written on one of the languages: TypeScript (that complies with JavaScript) and PowerShell. TypeScript will run on all platforms, whereas PowerShell will only run on the Windows platform. The extension also has a manifest that includes:
- the main extension file
- icons
- markdown file
- supporting modules
- and more
To start using a developed extension in continuous integration, you need to create your own Azure DevOps Organization, then you create Pipeline. Respectively, we create tasks for Pipeline that will be further mapped against tasks from our extension.
As you see in the picture, our pipeline is associated with the source code (it can be GitHub, Bit Bucket, Azure Repos, or something else). We take the source code from the very source — it could be your project on any language or a database script folder from which we will need to deploy this database.
The pipeline is also associated with the agent pool. Essentially, this is a connection with a machine on which the pipeline will be executed.
Azure DevOps Extension Anatomy
Our sample Azure DevOps extension will consist of the following files:
- The manifest file (vss-extension.json) is the main extension file.
- The task manifest files (task.json) that describe the task input parameters and the main entry point.
- Powershell.ps1 files. Those are the scripts that are executed as configured in task.json.
Note: you can either choose .js or .ps1 files. In our case, the PowerShell files will be used. - Secondary modules (for example, VstsTaskSdk).
- Icons (.png).
The picture down below showcases the folders and files structure of our extension.
Let’s shortly explain the structure of the picture above.
vss-extension.json manifest file
Vss-extension.json is the plugin basis and it describes general extension values (version, name, publisher, plugin icon, etc.). The manifest file also describes the place inside Azure DevOps where we want to embed our plugin. As it was mentioned, we embed the plugin into “pipeline tasks”.
Task folders
As you see in the picture, every task has its folder whereas task.json is the task’s basis.
The picture shows that our plugin has 3 tasks: Build, Test, and Publish. The number of tasks can be any. For convenience’s sake, each task is located in the folders that have their names. The folder contains 3 main files: task.json, powershell.ps1, and icon.png. Task.json is the very task, it contains its name, version, entry parameters, and what it should launch. In our case, it launches a PowerShell script that is described in a powershell.ps1 file, whereas icon.png is the task’s icon.
Ps_modules folders
These are the folders with secondary modules and we will use the VstsTaskSdk module. We will need it to interact with parameters, in other words, interact with UI.
Readme.md file
This is an unnecessary file. But if you’re going to make your extension public, the file will come handy for describing it that users will see at Azure Marketplace.
Extension creation, publishing, and usage cycles
To make the extension reach its destination — to be used in Azure DevOps in Pipeline Tasks, we need to complete the following steps:
- Create an Azure DevOps extension, a *.vsix file. The extension will get improved and updated further on.
- Create a publisher (this is done just once). It will enable publishing our extension in different versions.
- Upload it to Marketplace.
- Share the extension to a certain organization so it could be used in its pipeline.
In this article, I will focus on creating an Azure DevOps extension for the pipeline. To get more information about creating a publisher or an organization for hosting the extension, you should follow the link.
Preparing for the extension creation
Whether you create an extension for Azure DevOps Services or for Azure Pipeline Tasks, the following software should be installed:
- Install Node.js
- Install the extension packaging tool (TSF Cross-Platform Command-Line Interface) by running npm install -g tfx-cli from a command prompt
- Install the VstsTaskSdk module (by running an Install-Module -Name VstsTaskSdk -AllowClobber command)
Build Scaffolding
Let’s create a root folder for our extension, for example, DevopsExt (the root folder name doesn’t affect the final extension creation). While being in the DevopsExt root place, let’s create the following folder and empty files:
| readme.md
| vss-extension.json
|
+---images
| extension-icon.png
|
\---src
+---build
| powershell.ps1
| task.json
|
+---publish
| powershell.ps1
| task.json
|
\---test
powershell.ps1
task.json
Designing a vss-extension
As mentioned before, the manifest vss-extension.json file is the main file for the extension creation. To get more info about attributes description, their values, and file limitations, you should follow this link.
Down below you will see the attributes that we use. The following script will show our manifest vss-extension.json file’s content.
{
"manifestVersion": 1,
"id": "demo-devOps-extention",
"name": "Demo devOps Extention",
"version": "1.0.0",
"publisher": "demopublisher",
"targets": [
{
"id": "Microsoft.VisualStudio.Services"
}
],
"description": "A demo extension.",
"categories": [
"Azure Pipelines"
],
"icons": {
"default": "images/extension-icon.png"
},
"content": {
"details": {
"path": "readme.md"
}
},
"files": [
{
"path": "src/build",
"packagePath": "build"
},
{
"path": "src/publish",
"packagePath": "publish"
},
{
"path": "src/test",
"packagePath": "test"
}
],
"contributions": [
{
"id": "build-task",
"type": "ms.vss-distributed-task.task",
"targets": [
"ms.vss-distributed-task.tasks"
],
"properties": {
"name": "build"
}
},
{
"id": "test-task",
"type": "ms.vss-distributed-task.task",
"targets": [
"ms.vss-distributed-task.tasks"
],
"properties": {
"name": "test"
}
},
{
"id": "publish-task",
"type": "ms.vss-distributed-task.task",
"targets": [
"ms.vss-distributed-task.tasks"
],
"properties": {
"name": "publish"
}
}
]
}
Property — Description
manifestVersion — A number corresponding to the version of the manifest format (1).
id — The extension’s identifier. This is a string that must be unique among extensions from the same publisher. It must start with an alphabetic or numeric character and contain ‘A’ through ‘Z’, ‘a’ through ‘z’, ‘0’ through ‘9’, and ‘-‘ (hyphen).
version — A string specifying the version of an extension. It doesn’t need to be the same version as individual tasks in the extension. Should be in the format major.minor.patch, for example, 0.1.2 or 1.0.0.
name — A short, human-readable name of the extension. Limited to 200 characters.
description — Brief description of the extension. It will be displayed for the plugin when it’s searched in Marketplace.
publisher — The identifier of the publisher. This identifier must match the identifier the extension is published under. See Create and manage a publisher.
categories — An array of strings representing the categories your extension belongs to. At least one category must be provided and there is no limit to how many categories you may include. Valid values: Azure Repos, Azure Boards, Azure Pipelines, Azure Test Plans, and Azure Artifacts. In our case, it’s Azure Pipelines.
targets — The products and services supported by your integration or extension. See installation targets for more details.
icons — Dictionary of icons representing the extension. The icon should be 128×128 pixels in size and uploaded in one of these formats: BMP, GIF, EXIF, JPG, PNG, or TIFF.
files — The files section is where you reference any files you wish to include in your extension. You can add both folders and individual files.
Properties for the Files section:
- path — Path to a resource on disk, which can be relative to your root directory.
- addressable — (optional) Set to true if you want your file to be URL-addressable. Defaults to false.
- packagePath — (optional) Path to the resource within the package. Defaults to the relative path on disk from your root directory.
contributions — Each task MUST have a corresponding contribution.
Each contribution entry has the following properties:
- id — A reference ID (string) for the contribution. Each contribution’s ID must be unique within an extension.
- type — The contributionType ID of this contribution.
- description — (Optional) A string describing what the contribution is providing.
- targets — An array of contribution IDs that the contribution is targeting (contributing to).
- properties — (Optional) An object that includes properties for the contribution as defined in the contribution type.
For more information, see the contribution model overview.
DevOps task anatomy
As the folder structure shows, every task is located within its own subfolder. The task anatomy is the following:
|- SomeTask
|- task.json
|- icon.png
|- powershell.ps1
|- ps_modules
|- VstsTaskSdk
|- ...
Designing task.json
As you see from in folder structure, we have three task.json files that are in three folders: build, publish, and test — these are the tasks’ manifests.
You can see the build task script down below. The code will look the same for the other tasks, it’s just their identifiers, names, and UI entry fields that will be different.
{
"$schema": "https://raw.githubusercontent.com/Microsoft/azure-pipelines-task-lib/master/tasks.schema.json",
"id": "7a3fbeab-c278-458e-94f8-f8cb044d10d9",
"name": "DemoBuild",
"friendlyName": "Build a project",
"instanceNameFormat": "Build a project",
"description": "Builds your demo project",
"helpMarkDown": "",
"category": "Build",
"author": "Demo Company.",
"version": {
"Major": 1,
"Minor": 0,
"Patch": 5
},
"groups": [
{
"name": "inputGroup",
"displayName": "Source",
"isExpanded": true
},
{
"name": "outputGroup",
"displayName": "Output",
"isExpanded": true
}
],
"inputs": [
{
"name": "projectFolder",
"type": "string",
"label": "Project Folder",
"defaultValue": "",
"required": true,
"helpMarkDown": "Enter a full path to your project",
"groupName": "inputGroup"
},
{
"name": "outputFolder",
"type": "string",
"label": "Output Folder",
"defaultValue": "",
"required": true,
"helpMarkDown": "Enter output folder",
"groupName": "outputGroup"
}
],
"execution": {
"PowerShell3": {
"target": "powershell.ps1",
"platforms": [
"windows"
],
"workingDirectory": "$(currentDirectory)"
}
}
}
Designing powershell.ps1
We have three powershell.ps1 files that are located in three folders: build, publish, and test. These scripts are launched for our tasks.
The powershell.ps1 script lets us get UI parameters and run the list of required powershell commands for the task.
The following script shows a simplified example:
CmdletBinding()]
param()
$projectFolder = Get-VstsInput -Name 'projectFolder'
$outputFolder = Get-VstsInput -Name 'outputFolder'
Write-Host "Initial parameters:"
Write-Host "projectFolder = $projectFolder"
Write-Host "outputFolder = $outputFolder"
# Write your code that builds a project...
# ...
# ...
# ...
As you see in the script, we use Get-VstsInput commands from the VstsTaskSdk module to get UI parameter values. Then we can do any manipulations on these values.
Installing VstsTaskSdk
As it was mentioned, we use the VstsTaskSdk module to get the values from UI and use them in powershell.ps1 scripts. More specifically, we use the Get-VstsInput method.
To install and use this module, we need to create a ps_modules subfolder in our build, publish, and test folders. Once it’s done, your folder structure will look the following way:
| readme.md
| vss-extension.json
|
+---images
| extension-icon.png
|
\---src
+---build
| | powershell.ps1
| | task.json
| |
| \---ps_modules
+---publish
| | powershell.ps1
| | task.json
| |
| \---ps_modules
\---test
| powershell.ps1
| task.json
|
\---ps_modules
Now we need to install the VstsTaskSdk module from the Powershell Gallery on your computer. To do this, run the following command:
Install-Module -Name VstsTaskSdk -AllowClobber
Then we need to save this module into every ps_modules subfolder. To do this, run the following Powershell Save-Module command:
Save-Module -Name VstsTaskSdk -Path "FullPathToYour_ps_modules_Path" -Force
Once these steps are done, your folder structure will look the following way:
| readme.md
| vss-extension.json
|
+---images
| extension-icon.png
|
\---src
+---build
| | powershell.ps1
| | task.json
| |
| \---ps_modules
| \---VstsTaskSdk
| ...
|
+---publish
| | powershell.ps1
| | task.json
| |
| \---ps_modules
| \---VstsTaskSdk
| ...
|
\---test
| powershell.ps1
| task.json
|
\---ps_modules
\---VstsTaskSdk
...
Publishing and updating extensions
Now it’s time for you to start using the extension in Azure DevOps Pipeline. To do this, you need to upload and share the extension for the end-user organizations in Azure DevOps.
If you found this note useful, continue reading on the blog — https://codingsight.com/developing-azure-devops-extension/
Thank you and stay tuned!