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:
- 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.
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 namepublisher
: Publisher ID that the extension belongs.description
: Brief description of the extension.targets
: List of areas the extension is in charge. It SHOULD always beMicrosoft.VisualStudio.Services
as this is the Azure DevOps extension.categories
: List of services in Azure DevOps. SetAzure 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 areFree
,Paid
,Private
,Public
,Preview
. Of course,Free
andPaid
can't come together, and neither doesPrivate
andPublic
.icons
: Path and name of the icon. It can be any location and name likeimages/my-icon.png
. But it is recommended using the root folder and fixed name oficon.png
.content
: Path and name of the content that describes the extension. Like theicons
, it can be any location and name likedocs/readme.md
. But it is recommended using the root folder and fixed name ofREADME.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 theaddressable
value oftrue
, to be accessible from the Internet. - The current folder structure shows all the tasks are placed under the
src
folder, likesrc/install
andsrc/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 currentsrc/node_modules
folder MUST be copied toinstall/node_modules
anddeploy/node_modules
using thepackagePath
attribute.
- If the
links
: List of external URL links for more information. It generally includesoverview
,license
,repository
, andissues
.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.