The blog of Blog of Christian Bär

Creating an Azure Pipelines extension

Most of what an Azure Pipelines build or release should do can be accomplished by using the builtin tasks or tasks from the marketplace. Especially generic tasks like the Command Line task combined with the ability to install software on the build agents by using package managers (e.g. Chocolatey on Windows) provide a lot of creative freedom.

By implementing and publishing your own extension containing your custom tasks, you can consistently reuse functionality and make it easily accessible to other people inside and outside of your organization.

Sample extension

The extension used as an example in this blog post contains tasks to validate websites, namely a task that checks a website for broken links. Its source can be found in GitHub.

The code that makes up the essence of the extension’s task is implemented in TypeScript and is transpiled to JavaScript before the extension package is generated. Azure Pipelines runs it with Node.js when it executes the task.

For detailed information about the files and folders of the sample extension, check out the Appendix.

Create an extension using the sample extension

Prerequisites

  • Nodejs
  • TypeScript
    TypeScript can be installed using npm by running npm install -g typescript
  • TFS Cross Platform Command Line Interface (tfx-cli) to package your extensions.
    tfx-cli can be installed using npm by running npm i -g tfx-cli

Initialize folders and files

  1. Clone or download the sample extension’s repository.
  2. Adapt file and folder names as well as contents of package.json, vss-extension.json, task.json.
  3. Change contents of README.md, overview.md and license.md.
  4. Change the icons.
  5. In the console, from the root directory, execute
    npm install
  6. In the console, from each subfolder of src/tasks, execute
    npm install

Implement and build

  1. Implement some awesome functionality (in your *.ts files).
  2. To fill the dist folder with all the files needed to build extension package, execute in the console, from the root folder
    npm run-script build
  3. To build the extension package, execute in the console, from the root folder
    npm run-script package
    (no need to execute npm run-script build prior to that)

(Note: The definition of the build and package commands mentioned above can be found in the root directory’s package.json file.)

Publish to the marketplace

To publish your extension to the marketplace, go to https://marketplace.visualstudio.com/azuredevops and click on Publish extension. You will need to sign in and create a Publisher.

Updating an extension that’s already published and in use

When an extension is updated in the marketplace, its instances that are installed in Azure DevOps accounts will not automatically get that update. This is good for the production scenario because nobody wants their build to fail because there’s a breaking change in an extension.

However, this means that you have to uninstall and reinstall the extension in the DevOps account you use for testing while you are developing and testing your extension.
If there are no breaking changes in the task definitions (e.g. new required input fields), you don’t have to revisit your build definitions containing the tasks. The existing task instances also don’t get removed from build definitions by uninstalling and reinstalling the extension so don’t worry.

To uninstall the extension go to Organization settings > Extensions > Manage (https://dev.azure.com/someAccount/_settings/extensions). Hover over the extension and click More actions (the three dots).

Publish the extension using the tfx tool

As an alternative to publishing or updating your extension with Microsoft’s web UI, you can also use the tfx tool. Run tfx extension publish --help in the command line to find out how.

CI/CD for the extension using Azure DevOps

Thanks to the awesome existing extension CI/CD Tools for VSTS Extensions you can also build and publish your extension using Azure DevOps itself. It can even update instances of your extension to facilitate your extension development and testing.

Appendix

Sample Extension folder structure

build                          build scripts
  clean.js                     deletes the dist folder
  build.js                     copies files to the dist folder and calls npminstall.js
  npminstall.js                calls npm install for each src/tasks/*/package.json files
dist                           build output goes here
node_modules                   node modules used during development solely
src                            the source of the Azure DevOps extension
  tasks                        contains a subfolder for each build/release task
    brokenLinkChecker          brokenLinkChecker task subfolder
      node_modules             node modules used for the task
      brokenLinkCheckerTask.ts brokenLinkChecker task source code
      icon.png                 icon displayed in the UI where the task is configured
      package.json             lists node modules used for the task
      task.json                defines the task configuration UI and its startup file
    anotherTask               
      ...
  extension-icon.png           extension icon displayed in the market place (128*128)
  license.md
  overview.md                  content of the extension info page in the marketplace
  vss-extension.json           extension manifest file listing tasks, icon, version
package.json                   lists node modules used during development
README.md                      documentation about the repository (NOT the extension)
tsconfig.json                  TypeScript project file

Rationale behind the folder structure

As the extension is implemented in TypeScript which has to be transpiled to JavaScript, separate folders for the source (src folder containing TypeScript) and the transpiled JavaScript (dist folder) are used. Setting "rootDir":"src" and "outDir":"dist" in tsconfig.json make sure the resulting folder structure in dist is the same as in src. This and programmatically copying all other files from src to dist make for an easy solution to end up with all the extension files in the dist folder from which the package can then be created.

(The actual - but also complicated and not that important - reason for having an src and a dist folder as opposed to have neither of them is the following: All the files that make up a task must be self contained in a single folder. This includes the files in node_modules. Because the node_modules folder might potentially contain more files for development than for production, separating src from dist prevents bloating the extension package by including only the necessary modules.)

Additional information about some files

  • Extension manifest vss-extension.json
    • Official Reference
    • The version attribute must be changed before the extension can be updated in the marketplace. As an alternative the version can be provided when generating the extension package with the tfx tool.
    • Including a node_modules folder in the package by referencing it in the files attribute with the intention to have the contained modules available to all tasks does not work. Make sure all task subfolders are totally self contained.
  • Extension icon extension-icon.png
    • Best displayed when it is 128px * 128px
  • Task manifest task.json
    • Official JSON schema
    • The official documentation on tasks is either sparse or difficult to find. This extension makes use of input fields of types string, multiline and radio so check out the task’s source code for hints about how to use fields of these types.
    • Before the extension package is created, the task’s subfolder must contain all necessary files. This especially includes the node_modules. Azure DevOps does not exectute npm install while the extension is published in the marketplace or when the task is used in a build pipeline.
  • Task icon icon.png
    • Must be named icon.png
    • Must live in the same folder as the corresponding task.json
    • Best displayed when it is 64px * 64px
Share this post!