Add your extension

Open SSMS VSIX Gallery is a simple HTTP API. You can publish a .vsix file by uploading it from a script, a manual curl command, or a CI system like GitHub Actions or AppVeyor. Pick whichever fits your project.

  1. How publishing works
  2. Upload manually with curl
  3. Use PowerShell
  4. Automate with GitHub Actions (recommended)
  5. Use AppVeyor
  6. Managing your extension
  7. Unlisted extensions

How publishing works

Publishing is a single POST to https://www.vsixgallery.com/api/upload with the .vsix file as the request body. The server reads the manifest, stores the package, and returns a JSON response with the extension URL and a manage URL.

Optional headers and query-string parameters:

  • X-Manage-Token — a value of your choice that becomes the “password” for the extension’s manage page. If you omit it on the first upload, the server generates one and embeds it in the manage URL it returns. See Managing your extension.
  • ?repo=<url> — the source repository URL.
  • ?issuetracker=<url> — the issue tracker URL.
  • ?readmeUrl=<url> — a URL to a README.md that should be rendered on the extension details page.

Once that one HTTP call works, automating it with PowerShell, GitHub Actions, or AppVeyor is just glue around the same call.

Upload manually with curl

Useful for one-off uploads or for trying the API before wiring it into a build.

curl -X POST "https://www.vsixgallery.com/api/upload?repo=https://github.com/me/MyExtension" \
     -H "X-Manage-Token: my-secret-token" \
     --data-binary @MyExtension.vsix

The response is JSON containing the extension page URL and the manage URL. On the very first upload of a new extension ID, if you didn’t supply X-Manage-Token, the manage URL will contain a one-time token as a query string — save the URL.

Use PowerShell

First you must execute the VSIX script:

(new-object Net.WebClient).DownloadString("https://raw.github.com/madskristensen/ExtensionScripts/master/AppVeyor/vsix.ps1") | iex

That allows you to call methods to upload the .vsix extension file to the gallery:

Vsix-PublishToGallery

That will find all .vsix files in the working directory recursively and upload them. To specify the path, simply pass it in as the first parameter:

Vsix-PublishToGallery .\src\WebCompilerVsix\**\*.vsix

Automate with GitHub Actions

GitHub Actions is the recommended way to build and publish your SQL Server Management Studio extension. It provides free CI/CD directly in your GitHub repository, and the publish-vsixgallery action wraps the POST /api/upload call described above.

Create a file at .github/workflows/build.yaml in your repository with the following content:

Full workflow example (build, test, publish, release)
# yaml-language-server: $schema=https://www.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
      Solution: MyExtension.sln
      TestProject: tests\MyExtension.Tests\MyExtension.Tests.csproj
      VsixManifestPath: src\source.extension.vsixmanifest
      VsixManifestSourcePath: src\source.extension.cs

    steps:
    - uses: actions/checkout@v6

    - name: Setup MSBuild
      uses: microsoft/setup-msbuild@v3

    - name: Cache NuGet packages
      uses: actions/cache@v5
      with:
        path: ~/.nuget/packages
        key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.slnx', '**/*.sln', '**/*.csproj', '**/packages.config', '**/packages.lock.json', '**/nuget.config') }}
        restore-keys: |
          ${{ runner.os }}-nuget-

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

    - name: Restore NuGet packages
      run: msbuild ${{ env.Solution }} /v:m -restore /t:Restore

    - name: Build
      run: msbuild ${{ env.Solution }} /v:m /m /p:Configuration=${{ env.Configuration }} /p:DeployExtension=${{ env.DeployExtension }}

    - name: Run tests
      run: dotnet test ${{ env.TestProject }} --configuration ${{ env.Configuration }} --no-build --no-restore --verbosity normal --logger:"trx;LogFileName=test-results.trx"

    - name: Report test results
      uses: dorny/test-reporter@v3
      if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
      with:
        name: Test Results
        path: '**/test-results.trx'
        reporter: dotnet-trx

    - name: Upload artifact
      uses: actions/upload-artifact@v7
      with:
        name: ${{ github.event.repository.name }}.vsix
        path: src\bin\${{ env.Configuration }}\net48\MyExtension.vsix

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

    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 1

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

      - name: Upload to Open VSIX
        uses: madskristensen/publish-vsixgallery@v1
        with:
          vsix-file: ${{ github.event.repository.name }}.vsix
          manage-token: ${{ secrets.VS_PUBLISHER_ACCESS_TOKEN }}

      - name: Publish extension to Marketplace
        if: ${{ github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, '[release]') }}
        uses: madskristensen/publish-marketplace@v1.16
        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@v3
        with:
          body: release ${{ needs.build.outputs.version }}
          generate_release_notes: true
          tag_name: ${{ needs.build.outputs.version }}
          files: |
            **/*.vsix

Key GitHub Actions

Real-world examples using GitHub Actions:

Use AppVeyor

AppVeyor is a build server hosted in the cloud and it’s free.

After you’ve created an account, you can start doing automated builds. A really nice thing is that AppVeyor can automatically kick off a new build when you commit code to either GitHub, VSO or other code repositories.

To automatically upload your extension to vsixgallery.com when the build has succeeded, all you have to do is to add an appveyor.yml file to the root of your repository. The content of the file should look like this:

appveyor.yml example
version: 1.0.{build}

install:
  - ps: (new-object Net.WebClient).DownloadString("https://raw.github.com/madskristensen/ExtensionScripts/master/AppVeyor/vsix.ps1") | iex

before_build:
  - ps: Vsix-IncrementVsixVersion | Vsix-UpdateBuildVersion

build_script:
  - msbuild /p:configuration=Release /p:DeployExtension=false /p:ZipPackageCompressionLevel=normal /v:m

after_test:
  - ps: Vsix-PushArtifacts | Vsix-PublishToGallery

You might want to check out these real-world uses:

Managing your extension

Every extension has a manage page at https://www.vsixgallery.com/extension/<id>/manage where you (the publisher) can delete it. Access is gated by a per-extension manage token that acts as a password. The token is a soft “don’t let randos delete my listing” speed bump — not strong authentication.

How the token gets set depends on what you do at upload time. This applies to any upload method — manual curl, PowerShell, GitHub Actions, AppVeyor, etc.

  • First upload, no token supplied — the gallery auto-generates a token and embeds it in the manage URL it returns (printed by the GitHub Action’s run summary, or visible in the JSON response for manual uploads). That URL is shown only once, so save it somewhere safe.
  • First upload, token supplied — the gallery stores the value you supply (typically a CI secret). The manage URL won’t contain it.
  • Republish, no token supplied — the existing token stays in place. The manage URL won’t contain it.
  • Republish, token supplied — whatever value you pass becomes the new manage token, replacing whatever was on file. Lost your old token? Just republish with a fresh one and the new value takes over.

With the GitHub Action, the token is supplied via the manage-token input. With curl it’s the X-Manage-Token request header. Same value either way.

Unlisted extensions

An unlisted extension is one that you need to know the direct link to, otherwise it won’t show up anywhere. The only places it will show up are:

  • Direct link (example: vsixgallery.com/extension/[ID])
  • Author page
  • Author gallery feed
  • Extension gallery feed

That means it won’t show up in the usual places on this website such as:

  • Front page
  • Search results
  • Main gallery feed

To unlist an extension, simply add the tag “unlisted” in the .vsixmanifest file. The tags specified in the .vsixmanifest are not shown on either the Open SSMS VSIX Gallery nor inside SQL Server Management Studio, so it won’t be visible to anyone.

Here’s what it could look like:

<Tags>foo, bar, unlisted</Tags>