Add your extension
Open 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.
- How publishing works
- Upload manually with curl
- Use PowerShell
- Automate with GitHub Actions (recommended)
- Use AppVeyor
- Managing your extension
- Unlisted extensions
- Show a version badge
- Validation guidelines
How publishing works
Publishing is a single POST to https://www.vsixgallery.com/api/upload with the .vsix file as a multipart form upload. 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-or-path>— an HTTPS URL onraw.githubusercontent.comorgist.githubusercontent.com, or a path such asmain/README.mdrelative to the GitHub repository supplied byrepo.
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 --fail-with-body -X POST "https://www.vsixgallery.com/api/upload?repo=https://github.com/me/MyExtension" \
-H "X-Manage-Token: my-secret-token" \
-F "file=@MyExtension.vsix"The response is JSON containing the extension page URL, manage URL, and any non-blocking quality warnings found while inspecting the package. 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.
Example upload response with validation warnings
{
"id": "Example.Extension",
"name": "Example Extension",
"version": "1.0",
"manageUrl": "https://www.vsixgallery.com/extension/Example.Extension/manage",
"validation": [
{
"severity": "warning",
"code": "icon.invalid-dimensions",
"message": "The source icon is 48x48px. For best results, use 128x128 pixels or larger. Larger images are supported but are never displayed above 128x128 pixels."
}
]
}Warnings do not prevent publication. Invalid or unsafe packages are rejected with HTTP 400 and expose the same validation shape through the response’s Problem Details object. See the validation guidelines for every warning rule and its justification.
Upload with PowerShell
PowerShell 7 can upload the VSIX directly, without downloading or executing a remote script. Store the manage token in a secret environment variable rather than in source control.
$headers = @{ "X-Manage-Token" = $env:VSIX_GALLERY_TOKEN }
$form = @{ file = Get-Item ".\src\bin\Release\MyExtension.vsix" }
Invoke-RestMethod `
-Uri "https://www.vsixgallery.com/api/upload?repo=https://github.com/me/MyExtension" `
-Method Post `
-Headers $headers `
-Form $formAutomate with GitHub Actions
GitHub Actions is the recommended way to build and publish your Visual 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.
Add publishing to an existing workflow
If your workflow already builds a VSIX, add this step after the build. Store the manage token as a repository secret named VSIX_GALLERY_TOKEN.
- name: Upload to Open VSIX Gallery
uses: madskristensen/publish-vsixgallery@v1
with:
vsix-file: src\bin\Release\MyExtension.vsix
manage-token: ${{ secrets.VSIX_GALLERY_TOKEN }}If you need a complete starting point, create .github/workflows/build.yaml and adapt the following workflow to your solution, project paths, branch name, and publishing secrets:
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.VSIX_GALLERY_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
- microsoft/setup-msbuild — adds MSBuild to the runner’s PATH
- madskristensen/vsix-version-stamp — auto-increments the VSIX version number and stamps it into the manifest
- dorny/test-reporter — renders test results in the workflow run summary
- madskristensen/publish-vsixgallery — publishes to Open VSIX Gallery (writes a run summary with the extension link, badge, and a self-service manage link for deleting the extension)
- madskristensen/publish-marketplace — publishes to the Visual Studio Marketplace
- softprops/action-gh-release — creates a GitHub release and attaches the .vsix as an asset
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}
build_script:
- msbuild /p:configuration=Release /p:DeployExtension=false /p:ZipPackageCompressionLevel=normal /v:m
after_test:
- cmd: curl.exe --fail-with-body -X POST "https://www.vsixgallery.com/api/upload" -H "X-Manage-Token: %VSIX_GALLERY_TOKEN%" -F "file=@src\bin\Release\MyExtension.vsix"
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. Treat it like any other publishing credential: store it in a secret manager and do not commit it to source control.
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 hidden from discovery surfaces. You need its direct link to find it. It remains available from:
- Direct link (example: vsixgallery.com/extension/[ID])
- Extension gallery feed
That means it won’t show up in the usual places on this website such as:
- Front page
- Search results
- Author page
- Author gallery feed
- 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 VSIX Gallery nor inside Visual Studio, so it won’t be visible to anyone.
Here’s what it could look like:
<Tags>foo, bar, unlisted</Tags>
Show a version badge
Every extension exposes a live badge that always reflects the latest published version. Drop it in your README to show visitors the current version at a glance and link straight to the gallery page.
Two formats are available — just change the file extension:
https://www.vsixgallery.com/badge/<id>.svg— scalable vector, stays crisp at any size (recommended).https://www.vsixgallery.com/badge/<id>.png— rasterized image for places that don’t render SVG. Notably, the Visual Studio Marketplace only allows SVG badges from a short list of approved sources, so use the PNG variant there (and in other places that block third-party SVGs, such as some chat clients or release notes).
To embed the badge as a clickable link in Markdown:
[](https://www.vsixgallery.com/extension/<id>/)
Or with the PNG variant:
[](https://www.vsixgallery.com/extension/<id>/)
Replace <id> with your extension’s ID. The easiest way to grab the ready-made Markdown is the ⚙️ Actions menu on any extension page — pick Copy badge (SVG) or Copy badge (PNG) and it’s on your clipboard.