CI/CD pipeline for a Visual Studio extension (VSIX) using Azure DevOps

I am the author of the Meziantou.Analyzer, an open-source Roslyn analyzer that ensures the code you write follow some good practices in term of design and performance. This analyzer is published as a Visual Studio extension on the marketplace and as a NuGet package.

As I regularly update this project, I want the changes to be published quickly on the marketplace and on NuGet. Of course I don't want to do it manually as it is error prone and time consuming. So, I've used Azure Pipelines to create a CI/CD pipeline. The idea is that each commit triggers the pipeline, and each commit on master publishes the new version automatically.

Main steps of the CI/CD pipeline

The pipeline contains 2 parts: the build definition and the release definition.

The build definition contains the following steps:

  1. Update the Version attribute in the VSIX manifest
  2. Build the project and create packages
  3. Run the tests

The release definition contains the following steps:

  1. Download the artifacts of the build pipeline
  2. Publish the VSIX package to the marketplace

Project structure

We'll need some script for the build in addition to the source files. Here's the final structure of my solution:

build/
├── extension-manifest.json
├── publish-vsix.ps1
└── update-version.ps1
src/
├── Meziantou.Analyzer/Meziantou.Analyzer.csproj
└── Meziantou.Analyzer.Vsix/Meziantou.Analyzer.Vsix.csproj
tests/
└── Meziantou.Analyzer.Test/Meziantou.Analyzer.Test.csproj

azure-pipelines.yml
Meziantou.Analyzer.sln
README.md

We'll see the content of the build files later.

The build definition

The build definition can be written in yaml, so the build definition is versioned with the code which is great. Create a file named azure-pipelines.yml in the root folder of the project:

trigger:
- '*' # Run the pipeline for each commit

pool:
  vmImage: 'windows-2019' # Windows Server 2019 with Visual Studio 2019

variables:
  # patch will be incremented at each build. This is useful to create a unique build version.
  patch: $[counter('VersionCounter', 0)]
  solution: '**/*.sln'
  buildPlatform: 'Any CPU'
  buildConfiguration: 'Release'
  DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1

name: 1.0.$(patch) # Set the value of $(Build.BuildNumber)

steps:
- task: PowerShell@2
  displayName: Update version in the vsix manifest
  inputs:
    filePath: 'build\update-version.ps1'
    arguments: '$(Build.BuildNumber)'
    pwsh: true

- task: NuGetCommand@2
  inputs:
    command: 'restore'

- task: VSBuild@1
  inputs:
    solution: '**\*.sln'
    maximumCpuCount: true
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'

- task: VSTest@2
  inputs:
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'

# Publish all needed files for the Release pipeline
- task: CopyFiles@2
  inputs:
    SourceFolder: '$(Build.SourcesDirectory)'
    Contents: |
      README.md
      build/**
      **/*.vsix
    TargetFolder: '$(Build.ArtifactStagingDirectory)'

- task: PublishPipelineArtifact@0
  inputs:
    artifactName: 'drop'
    targetPath: '$(Build.ArtifactStagingDirectory)'

Here's the content of the file build\update-version.ps1. It's a PowerShell script as it allows to read and edit XML files very easily. The goal is to load the vsixmanifest file and update the version attribute with the version defined in the pipeline.

$version = $args[0]
Write-Host "Set version: $version"

# TODO: Replace the path with the path to your vsixmanifest file
$FullPath = Resolve-Path $PSScriptRoot\..\src\Meziantou.Analyzer.Vsix\source.extension.vsixmanifest
Write-Host $FullPath
[xml]$content = Get-Content $FullPath
$content.PackageManifest.Metadata.Identity.Version = $version
$content.Save($FullPath)

If everything's ok, you should now have a green build!

The release definition

Azure Pipeline doesn't provide a task to publish a VSIX package to the marketplace. This means we'll have to do it manually. First, you need to find VsixPublisher.exe on the machine. You could hard-code the path, but this is not recommended as you don't know where Visual Studio is installed on the build machines. Instead, you should use vswhere to find where Visual Studio is installed, and combine this path with the relative path of VsixPublisher.exe in the installation folder. Then, you can call VsixPublisher.exe with the required arguments: the path to the VSIX, the path to the manifest file and the Personal Access Token.

I've created a PowerShell script to do that. Here's the content of the file build\publish-vsix.ps1:

$PersonalAccessToken = $args[0]
# TODO: Replace the path with the path to your VSIX file
$VsixPath = "$PSScriptRoot\..\src\Meziantou.Analyzer.Vsix\bin\Release\Meziantou.Analyzer.vsix"
$ManifestPath = "$PSScriptRoot\extension-manifest.json"

# Find the location of VsixPublisher
$Installation = & "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -latest -prerelease -format json | ConvertFrom-Json
$Path = $Installation.installationPath

Write-Host $Path
$VsixPublisher = Join-Path -Path $Path -ChildPath "VSSDK\VisualStudioIntegration\Tools\Bin\VsixPublisher.exe" -Resolve

Write-Host $VsixPublisher

# Publish to VSIX to the marketplace
& $VsixPublisher publish -payload $VsixPath -publishManifest $ManifestPath -personalAccessToken $PersonalAccessToken -ignoreWarnings "VSIXValidatorWarning01,VSIXValidatorWarning02,VSIXValidatorWarning08"

Then, you need to create the extension manifest file to publish the extension. Here's the manifest of my extension (build\extension-manifest.json):

{
    "$schema": "http://json.schemastore.org/vsix-publish",
    "categories": [
        "Coding"
    ],
    "identity": {
        "internalName": "Meziantou-Analyzer",
        "tags": [
            "analyzer"
        ]
    },
    "overview": "../README.md",
    "priceCategory": "free",
    "publisher": "Meziantou",
    "private": false,
    "qna": true,
    "repo": "https://github.com/meziantou/Meziantou.Analyzer"
}

Now let's create the release definition:

Select the empty template. Then, click on Add an artifact and select the artifact of your build definition. Then, click on the trigger and enable the "Continuous deployment trigger" and filter on the branch "master":

You need to be authenticated to be able to publish an extension to the marketplace. So, you need to generate a Personal Access Token (PAT) with the required access as described in this documentation page for VS Code (but it's the same for a VS extension). You can then add the value in a secret variable of the release definition:

Finally you need to add a stage and add the PowerShell step:

Don't forget to click on Save. You can now click on the "Create Release" button. Once the release is created, you can check it works by looking at its status:

You should see the new version on the marketplace:

You can check the code of my project on GitHub: https://github.com/meziantou/Meziantou.Analyzer

Enjoy this blog? Buy Me A Coffee Donate with PayPal

Comments

Dmitry Pavlov -

O.M.G. This is awesome!

Leave a reply