Automated publishing

Visual Studio Extensibility Cookbook

Automating the build and publish process for your extension saves time and ensures every release is consistent. This guide shows the recommended approach using GitHub Actions with community-maintained actions that handle version stamping, building, and publishing.

The publish manifest

Create a vs-publish.json file in the root of your repo. This tells the Marketplace about your extension’s metadata. For a VSIX extension, most identity fields come from the .vsixmanifest - you only need to set the internalName:

{
  "$schema": "http://json.schemastore.org/vsix-publish",
  "categories": [ "other" ],
  "identity": {
    "internalName": "MyExtension"
  },
  "overview": "README.md",
  "publisher": "YourPublisherName",
  "repo": "https://github.com/YourName/MyExtension"
}

If your Marketplace listing includes images, add them to assetFiles:

{
  "assetFiles": [
    {
      "pathOnDisk": "art/screenshot.png",
      "targetPath": "art/screenshot.png"
    }
  ]
}

For full details on the manifest format, see Publishing via command line on Microsoft Learn.

Create a Personal Access Token

You need a Personal Access Token (PAT) to authenticate with the Marketplace.

  1. Go to dev.azure.com and sign in with the Microsoft account that owns your Marketplace publisher
  2. Open User Settings -> Personal access tokens -> New Token
  3. Set the organization to All accessible organizations
  4. Under scopes, select Marketplace -> Manage
  5. Create the token and copy it immediately

In your GitHub repository, go to Settings -> Secrets and variables -> Actions and add the token as a secret named VS_PUBLISHER_ACCESS_TOKEN.

GitHub Actions workflow

The recommended workflow uses two jobs: build (runs on every push/PR) and publish (runs only on push to master or manual dispatch). Publishing to the Marketplace happens when the commit message contains [release] or the workflow is triggered manually.

Create .github/workflows/build.yaml:

# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
name: "Build"
permissions:
    actions: write
    contents: write

on:
    push:
        branches: [master]
    pull_request:
        branches: [master]
    workflow_dispatch:

jobs:
    build:
        outputs:
            version: ${{ steps.vsix_version.outputs.version-number }}
        name: Build
        runs-on: windows-latest
        env:
            Configuration: Release
            DeployExtension: False
            VsixManifestPath: src\source.extension.vsixmanifest
            VsixManifestSourcePath: src\source.extension.cs

        steps:
        - uses: actions/checkout@v4

        - name: Setup .NET build dependencies
          uses: timheuer/bootstrap-dotnet@v1
          with:
              nuget: 'false'
              sdk: 'false'
              msbuild: 'true'

        - name: Increment VSIX version
          id: vsix_version
          uses: timheuer/vsix-version-stamp@v2
          with:
              manifest-file: ${{ env.VsixManifestPath }}
              vsix-token-source-file: ${{ env.VsixManifestSourcePath }}

        - name: Build
          run: msbuild /v:m -restore /p:OutDir=\_built

        - name: Upload artifact
          uses: actions/upload-artifact@v4
          with:
              name: ${{ github.event.repository.name }}.vsix
              path: /_built/**/*.vsix

    publish:
        if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }}
        needs: build
        runs-on: windows-latest

        steps:
            - uses: actions/checkout@v4

            - name: Download Package artifact
              uses: actions/download-artifact@v4
              with:
                  name: ${{ github.event.repository.name }}.vsix

            - name: Upload to Open VSIX
              uses: timheuer/openvsixpublish@v1
              with:
                  vsix-file: ${{ github.event.repository.name }}.vsix

            - name: Publish extension to Marketplace
              if: ${{ github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, '[release]') }}
              uses: cezarypiatek/VsixPublisherAction@1.0
              with:
                  extension-file: '${{ github.event.repository.name }}.vsix'
                  publish-manifest-file: 'vs-publish.json'
                  personal-access-code: ${{ secrets.VS_PUBLISHER_ACCESS_TOKEN }}

            - name: Tag and release
              if: ${{ github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, '[release]') }}
              id: tag_release
              uses: softprops/action-gh-release@v2
              with:
                  body: release ${{ needs.build.outputs.version }}
                  generate_release_notes: true
                  tag_name: ${{ needs.build.outputs.version }}
                  files: |
                      **/*.vsix

How it works

The workflow has two jobs:

Build - runs on every push and PR to master. Restores, version-stamps, builds, and uploads the VSIX as an artifact.

Publish - runs only on push to master or manual dispatch. It always publishes to Open VSIX Gallery so nightly users get updates. When the commit message contains [release] (or on manual dispatch), it also publishes to the VS Marketplace and creates a tagged GitHub release.

Key actions used

timheuer/bootstrap-dotnet - sets up MSBuild and .NET dependencies.

timheuer/vsix-version-stamp - auto-increments the VSIX version based on the build number.

timheuer/openvsixpublish - publishes to the Open VSIX Gallery.

cezarypiatek/VsixPublisherAction - wraps VsixPublisher.exe for Marketplace publishing.

softprops/action-gh-release - creates a GitHub release with the VSIX attached.

Publishing a release

To publish to the Marketplace, include [release] in your commit message:

git commit -m "Fix bug in outlining [release]"
git push

Or trigger the workflow manually from the Actions tab in GitHub.

Key tips

Set DeployExtension to False in CI to prevent the build from trying to launch the VS experimental instance.

Always sign your VSIX - unsigned extensions don’t auto-update for users. See the publishing checklist.

Keep PATs short-lived - set an expiration and rotate regularly.

Adjust the VsixManifestPath and VsixManifestSourcePath env variables to match your project structure.

Additional resources

Last updated: