7 min read

Building Azure DevOps Extension on Azure DevOps - Manual Publish

Justin Yoo

In my previous post, we've discussed how to register publishers to Marketplace. This post will discuss how to package the extension and manually publish it to the marketplace.

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.

Packaging Extension

Extension MUST be packaged before it is published to the marketplace. It has .vsix file extension, which is basically another type of .zip file. To generate this package, we need to create a manifest file, vss-extension.json. Let's start from there.

Manifest File

The manifest file, vss-extension.json MUST be placed in the root folder of the extension. Once it is created, the folder structure might look like:

Fill in the file like below:

{
"manifestVersion": 1,
"id": "{{ Extension ID }}",
"version": "{{ Extension Version }}",
"name": "{{ Extenson Name }}",
"publisher": "{{ Publisher ID }}",
"description": "{{ Description }}",
"targets": [
{
"id": "Microsoft.VisualStudio.Services"
}
],
"categories": [
"Azure Pipelines"
],
"tags": [
"netlify"
],
"galleryFlags": [
"Free",
"Public"
],
"icons": {
"default": "icon.png"
},
"content": {
"details": {
"path": "README.md"
}
},
"files": [
{
"path": "images",
"addressable": true
},
{
"path": "src/install",
"packagePath": "install"
},
{
"path": "src/deploy",
"packagePath": "deploy"
},
{
"path": "src/node_modules",
"packagePath": "install/node_modules"
},
{
"path": "src/node_modules",
"packagePath": "deploy/node_modules"
}
],
"links": {
"overview": {
"uri": "https://github.com/aliencube/AzureDevOps.Extensions/blob/master/README.md"
},
"license": {
"uri": "https://github.com/aliencube/AzureDevOps.Extensions/blob/master/LICENSE"
},
"repository": {
"uri": "https://github.com/aliencube/AzureDevOps.Extensions"
},
"issues": {
"uri": "https://github.com/aliencube/AzureDevOps.Extensions/issues"
}
},
"repository": {
"type": "git",
"uri": "https://github.com/aliencube/AzureDevOps.Extensions"
},
"badges": [
{
"href": "https://dev.azure.com/aliencube/AzureDevOps.Extensions/_build/latest?definitionId=-1",
"uri": "https://dev.azure.com/aliencube/AzureDevOps.Extensions/_apis/build/status/%5Bnetlify%5D%20dev%2C%20feature%2C%20hotfix",
"description": "Build Status"
}
],
"contributions": [
{
"id": "install-task",
"type": "ms.vss-distributed-task.task",
"targets": [
"ms.vss-distributed-task.tasks"
],
"properties": {
"name": "install"
}
},
{
"id": "deploy-task",
"type": "ms.vss-distributed-task.task",
"targets": [
"ms.vss-distributed-task.tasks"
],
"properties": {
"name": "deploy"
}
}
]
}

It looks overwhelming, but it's not that complicated. Let's have a look at each attribute.

  • id: Extension ID. This MUST be unique across the whole marketplace. Only alphanumeric letters and hyphen are allowed.
  • version: Extension version. It doesn't need to be the same version as individual tasks in the extension.
  • name: Extension name
  • publisher: Publisher ID that the extension belongs.
  • description: Brief description of the extension.
  • targets: List of areas the extension is in charge. It SHOULD always be Microsoft.VisualStudio.Services as this is the Azure DevOps extension.
  • categories: List of services in Azure DevOps. Set Azure Pipelines, as this extension is for Azure Pipelines.
  • tags: List of tags for search in the marketplace.
  • galleryFlags: List of flags how the extension is published. Possible values are Free, Paid, Private, Public, Preview. Of course, Free and Paid can't come together, and neither does Private and Public.
  • icons: Path and name of the icon. It can be any location and name like images/my-icon.png. But it is recommended using the root folder and fixed name of icon.png.
  • content: Path and name of the content that describes the extension. Like the icons, it can be any location and name like docs/readme.md. But it is recommended using the root folder and fixed name of README.md.
  • files: List of files that consist of this extension.

    • If the README.md needs some images, that images themselves or folder containing the images can be included. In this case, the files or folder MUST have the addressable value of true, to be accessible from the Internet.
    • The current folder structure shows all the tasks are placed under the src folder, like src/install and src/deploy. However, the package expects all the task folders MUST be under the root folder. Therefore, packagePath attribute is used to adjust the location.
    • Each task expects node_modules folder for proper invocation. Therefore, the current src/node_modules folder MUST be copied to install/node_modules and deploy/node_modules using the packagePath attribute.
  • links: List of external URL links for more information. It generally includes overview, license, repository, and issues.
  • repository: Repository URL, if the extension is open-sourced.
  • badges: Build/release status badge URL.
  • contributions: Each task MUST have its corresponding contribution.

It's really a brief, but if you need more details, refer to this page.

Packaging Extension

Now, we've got the manifest file. Let's create the package. It requires to install Azure DevOps Extension CLI (tfx-cli). Enter the following command to install tfx-cli on your local machine.

npm install -g tfx-cli

After installing CLI, run the following command at the location where the vss-extension.json is.

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

Obviously, there are many more commands on tfx-cli, but we only need this command above. If you need to more about tfx-cli, refer to this page.

There might be naming confusion around many CLIs. This tfx-cli is for Azure DevOps Extension related. If you want Azure DevOps itself through CLI, use Azure DevOps CLI Extension, which is one of the extensions of Azure CLI.

Now, we've got the package file! The package file name always looks like [Publisher ID].[Extension ID]-[Version].vsix.

Publishing Extension

It's time to publish. We're going to publish it through the publisher of aliencube-dev.

When you go into the publisher manager page, aliencube-dev has nothing published yet. Click the + New Extension button to publish a new extension.

You'll be asked to upload the package file. As we just created the package, upload it.

Oops! Upload failure! How come? As the error message says, we create the package for the publisher of aliencube, which is not correct for now. In other words, we need to update the manifest file, vss-extension.json to declare the correct publisher.

Update the vss-extension.json file with correct publisher ID, package it again with tfx-cli and upload it. Hmmm, another error occurred. What does this mean this time?

As the aliencube-dev publisher hasn't officially verified, it can't publish any public-facing extension. To sort this out, either the publisher is verified by Microsoft, or upload a private package. The intention of using aliencube-dev is only to deal with private extensions. So, just update vss-extension.json to create a private extension. Change the galleryFlags attribute values from Public to Private, package it and upload it.

Now, it's all good!

Sharing Extension

The extension uploaded is private, which is not publicly shown. Unless it's visible, we can't download and use it. Therefore, we can share the private extension with designated Azure DevOps organisations. Click the three dots button then click Share/Unshare.

It shows to enter the Azure DevOps organisation to whare this private extension. I've got an Azure DevOps organisation only for the extension testing only, https://dev.azure.com/aliencube-dev.

It's now shared. Open the Azure DevOps organisation and go to the organisation settings page. In the settings, open the Extensions tab then click the Shared tab in the middle.

Now you can find out the shared extension. Install it and you'll be able to see the following screen.

Now, the extension has been installed. Let's run the task in a pipeline.

Running Task in Pipeline

In a release pipeline, search netlify and you'll be able to find two tasks that we've built.


So far, we've created a package of Azure DevOps extension that we built, published, shared, installed and used it. All these steps were manual, by the way.

Let's think about the errors we've met during the package publishing. We had to modify the manifest file for testing. If we publish it publicly, we have to modify the manifest file again, which is not nice. I'm not sure this is efficient. Is there any other way to minimise manual human intervention?

The next post will discuss how to automate the entire publishing process through CI/CD pipelines using Azure DevOps.

More Readings