Build and publish Visual Studio extensions with GitHub Actions

A friend of mine asked me how we build and publish our Visual Studio extension. His initial thought was that the process is manual and since the code is in a private repository, here's a quick walkthrough of building and publishing Visual Studio extensions with GitHub Actions.

Build and publish Visual Studio extensions with GitHub Actions

Before I start to write build scripts, I quickly want to put a few words on the various kind of extensions out there. There are extensions for Visual Studio, Visual Studio Code, and Azure DevOps. We currently offer both an extension for VS as well as a couple of extensions for Azure DevOps. All of them are available in the Visual Studio Marketplace. How you need to build and deploy each extension differs from VS extensions to Azure DevOps extensions. The instructions provided as part of this post are for VS extensions only. If anyone is interested, I can write a post about Azure DevOps extensions as well. The good thing there is that we already provide examples of doing this as part of the public GitHub repositories (like this one).

Let's start coding. For the rest of this post, I'll create a build script for an imaginary GitHub repository including a VS extension. If you want to try and build a VS extension from scratch, check out Building a Stack Overflow browser as a VS extension. Start by going to the Actions tab on the repository and click the set up a workflow yourself link. This will create a blank file named main.yml:

Create build script

You can write your build script in the online editor on GitHub or commit the file and launch your favorite YAML editor locally.

Start by giving the script a name and tell GitHub Actions when to launch the script:

name: Build extension

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

Nothing VS extension-specific code yet. This build script will automatically run when commits are made on the main branch as well as when someone created a pull request.

Next, we'll create the job to run:

jobs:
  build:
    runs-on: windows-latest
    steps:

The script will launch on an image containing Windows. I'm pretty sure extensions still need to be built on Windows, but feel free to play with it. Let's fill in the first steps:

- name: Checkout the code
  uses: actions/checkout@v2
- name: Add nuget to PATH
  uses: nuget/setup-nuget@v1
- name: Add msbuild to PATH
  uses: microsoft/setup-msbuild@v1.0.2

The first line will check out the newest code. The second line will set up NuGet as part of the script. We will use NuGet in a bit to restore packages. We could probably use dotnet to restore the code here but let's just stay in this kind of obsolete world of nuget and msbuild commands 😊 The final two lines set up MSBuild which is used to build the extensions.

Would your users appreciate fewer errors?

➡️ Reduce errors by 90% with elmah.io error logging and uptime monitoring ⬅️

Before we start to build the extension project, I like to have GitHub Actions version the extension automatically. This can be done with a bit of PowerShell magic:

- name: Update version
  run: |
    (Get-Content -Path MyCoolVSExtension\source.extension.vsixmanifest) |
      ForEach-Object {$_ -Replace '1.0.0', '1.0.${{ github.run_number }}'} |
        Set-Content -Path MyCoolVSExtension\source.extension.vsixmanifest
    (Get-Content -Path MyCoolVSExtension\Properties\AssemblyInfo.cs) |
      ForEach-Object {$_ -Replace '1.0.0', '1.0.${{ github.run_number }}'} |
        Set-Content -Path MyCoolVSExtension\Properties\AssemblyInfo.cs
    (Get-Content -Path MyCoolVSExtension\MyCoolVSExtension.cs) |
      ForEach-Object {$_ -Replace '1.0.0', '1.0.${{ github.run_number }}'} |
        Set-Content -Path MyCoolVSExtension\MyCoolVSExtension.cs

For this example, I'm updating the version string 1.0.0 found in three different files with 1.0. plus a current run counter from GitHub Actions. This way, the patch version is incremented on each build. If I want to increase the major or minor version, I'll update the version number in the three files manually.

It's time for building the code:

- name: Restore
  run: nuget restore
- name: Build
  run: msbuild /p:configuration=Release /p:DeployExtension=false /p:ZipPackageCompressionLevel=normal

The first step restores all of the needed packages. The second step uses msbuild to build the extension. I'm passing in a couple of properties. The first one will build the extension in Release mode. The DeployExtension property will tell MSBuild to not actually install the generated VSIX file in a local Visual Studio. You would normally do this when developing the extension locally but not on the build server. TBH, the ZipPackageCompressionLevel property is just there because Mads Kristensen has that on all of his extensions. I'm unsure why but I usually just copy what Mads does 😂

The final step of the build script is to upload the generated VSIX file as an artifact:

- uses: actions/upload-artifact@v2
  with:
    name: MyCoolVSExtension.vsix
    path: MyCoolVSExtension\bin\Release\MyCoolVSExtension.vsix

That's it! All commits are now automatically built and uploaded as artifacts on GitHub. You can download and install the VSIX directly from GitHub to test it before you upload it to the Visual Studio Marketplace.

Here's the full main.yml file:

name: Build extension

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: windows-latest
    steps:
      - name: Checkout the code
        uses: actions/checkout@v2
      - name: Add nuget to PATH
        uses: nuget/setup-nuget@v1
      - name: Add msbuild to PATH
        uses: microsoft/setup-msbuild@v1.0.2
      - name: Update version
        run: |
          (Get-Content -Path MyCoolVSExtension\source.extension.vsixmanifest) |
            ForEach-Object {$_ -Replace '1.0.0', '1.0.${{ github.run_number }}'} |
              Set-Content -Path MyCoolVSExtension\source.extension.vsixmanifest
          (Get-Content -Path MyCoolVSExtension\Properties\AssemblyInfo.cs) |
            ForEach-Object {$_ -Replace '1.0.0', '1.0.${{ github.run_number }}'} |
              Set-Content -Path MyCoolVSExtension\Properties\AssemblyInfo.cs
          (Get-Content -Path MyCoolVSExtension\MyCoolVSExtension.cs) |
            ForEach-Object {$_ -Replace '1.0.0', '1.0.${{ github.run_number }}'} |
              Set-Content -Path MyCoolVSExtension\MyCoolVSExtension.cs
      - name: Restore
        run: nuget restore
      - name: Build
        run: msbuild /p:configuration=Release /p:DeployExtension=false /p:ZipPackageCompressionLevel=normal
      - uses: actions/upload-artifact@v2
        with:
          name: MyCoolVSExtension.vsix
          path: MyCoolVSExtension\bin\Release\MyCoolVSExtension.vsix

Make sure to also check out our List of the Best Free Visual Studio Extensions.

elmah.io: Error logging and Uptime Monitoring for your web apps

This blog post is brought to you by elmah.io. elmah.io is error logging, uptime monitoring, deployment tracking, and service heartbeats for your .NET and JavaScript applications. Stop relying on your users to notify you when something is wrong or dig through hundreds of megabytes of log files spread across servers. With elmah.io, we store all of your log messages, notify you through popular channels like email, Slack, and Microsoft Teams, and help you fix errors fast.

See how we can help you monitor your website for crashes Monitor your website