13 KiB
WinGet Publishing Guide
Complete step-by-step guide for publishing your Command Palette extension to WinGet for winget install discovery and installation.
Why WinGet?
Publishing to WinGet enables:
- Users to install via
winget install YourPublisher.YourExtension - Discovery directly inside Command Palette's built-in browse experience
- Automatic update detection via WinGet manifests
Step 1: Prepare the Project for Unpackaged Distribution
WinGet distribution uses an unpackaged (EXE-based) build instead of MSIX.
Update .csproj
Remove any existing <PublishProfile> property and add unpackaged mode:
<PropertyGroup>
<!-- Remove or comment out this line if present: -->
<!-- <PublishProfile>win-$(Platform)</PublishProfile> -->
<!-- Add this for unpackaged distribution: -->
<WindowsPackageType>None</WindowsPackageType>
</PropertyGroup>
Note Your CLSID
Find the [Guid("...")] attribute in your main .cs file (e.g., SampleExtension.cs):
[Guid("YOUR-GUID-HERE")]
public sealed partial class SampleExtension : IExtension
You'll need this exact GUID for the installer script. It must match across all files.
Step 2: Create Installer Scripts
Inno Setup Script: setup-template.iss
Create this file in your project root. Replace all TODO placeholders with your values:
; Inno Setup script for Command Palette extension
#define MyAppName "TODO_YOUR_EXTENSION_NAME"
#define MyAppVersion "TODO_YOUR_VERSION"
#define MyAppPublisher "TODO_YOUR_PUBLISHER_NAME"
#define MyAppURL "TODO_YOUR_PROJECT_URL"
#define MyAppCLSID "TODO_YOUR_CLSID_WITH_BRACES"
; Example CLSID: {12345678-1234-1234-1234-123456789012}
[Setup]
AppId={#MyAppCLSID}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
DefaultDirName={autopf}\{#MyAppName}
OutputBaseFilename={#MyAppName}_{#MyAppVersion}_{#SetupSetting("ArchitecturesAllowed")}
Compression=lzma
SolidCompression=yes
WizardStyle=modern
PrivilegesRequired=lowest
OutputDir=Installer
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
[Files]
Source: "publish\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
[Registry]
; Register the COM server for Command Palette discovery
Root: HKCU; Subkey: "Software\Classes\CLSID\{#MyAppCLSID}"; ValueType: string; ValueName: ""; ValueData: "{#MyAppName}"; Flags: uninsdeletekey
Root: HKCU; Subkey: "Software\Classes\CLSID\{#MyAppCLSID}\InprocServer32"; ValueType: string; ValueName: ""; ValueData: "{app}\{#MyAppName}.dll"; Flags: uninsdeletekey
Root: HKCU; Subkey: "Software\Classes\CLSID\{#MyAppCLSID}\InprocServer32"; ValueType: string; ValueName: "ThreadingModel"; ValueData: "Both"; Flags: uninsdeletekey
[UninstallDelete]
Type: filesandordirs; Name: "{app}"
Important: The
AppIdmust use your CLSID wrapped in braces. The registry entries register your extension's COM server so Command Palette can discover it.
Build Script: build-exe.ps1
Create this PowerShell script in your project root:
<#
.SYNOPSIS
Builds EXE installers for x64 and ARM64 using dotnet publish and Inno Setup.
.DESCRIPTION
Publishes the project for both architectures, then runs Inno Setup to create
EXE installers suitable for WinGet submission.
#>
param(
[string]$Configuration = "Release",
[string]$Version = "0.0.1"
)
$ErrorActionPreference = "Stop"
$projectName = (Get-ChildItem -Filter "*.csproj" | Select-Object -First 1).BaseName
if (-not $projectName) {
Write-Error "No .csproj file found in the current directory."
exit 1
}
$architectures = @("x64", "arm64")
foreach ($arch in $architectures) {
Write-Host "`n=== Building $arch ===" -ForegroundColor Cyan
# Publish
Write-Host "Publishing for $arch..."
dotnet publish -c $Configuration -r "win-$arch" -o "publish" --self-contained=false
if ($LASTEXITCODE -ne 0) {
Write-Error "dotnet publish failed for $arch"
exit 1
}
# Create installer
Write-Host "Creating installer for $arch..."
$issFile = "setup-template.iss"
if (-not (Test-Path $issFile)) {
Write-Error "Inno Setup script not found: $issFile"
exit 1
}
$archFlag = if ($arch -eq "arm64") { "arm64" } else { "x64" }
& "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" `
/DMyAppVersion="$Version" `
/DArchitecturesAllowed="$archFlag" `
$issFile
if ($LASTEXITCODE -ne 0) {
Write-Error "Inno Setup failed for $arch"
exit 1
}
# Clean publish directory for next architecture
Remove-Item -Recurse -Force "publish" -ErrorAction SilentlyContinue
Write-Host "=== $arch complete ===" -ForegroundColor Green
}
Write-Host "`nInstallers created in the 'Installer' directory:" -ForegroundColor Cyan
Get-ChildItem -Path "Installer" -Filter "*.exe" | ForEach-Object { Write-Host " $_" }
Step 3: Build EXE Installers
Run the build script from your project directory:
.\build-exe.ps1
This produces two EXE files in the Installer directory:
Installer\YourExtension_0.0.1_x64.exe
Installer\YourExtension_0.0.1_arm64.exe
Verify both installers work by running them locally and confirming your extension appears in Command Palette.
Step 4: Create a GitHub Release
Tag your repository with the version and create a release with the EXE files:
# Tag the release
git tag -a v0.0.1 -m "Release v0.0.1"
git push origin v0.0.1
# Create release and upload assets (requires GitHub CLI)
gh release create v0.0.1 `
"Installer\YourExtension_0.0.1_x64.exe" `
"Installer\YourExtension_0.0.1_arm64.exe" `
--title "v0.0.1" `
--notes "Initial release of YourExtension for Command Palette."
After creating the release, copy the download URLs for both EXE files — you'll need them for the WinGet submission.
Step 5: Submit to WinGet
Use wingetcreate to generate a WinGet manifest and submit a pull request:
wingetcreate new "<URL_TO_x64.exe>" "<URL_TO_arm64.exe>"
wingetcreate will interactively prompt you for:
| Prompt | Example Value |
|---|---|
| PackageIdentifier | YourPublisher.YourExtension |
| PackageVersion | 0.0.1 |
| PackageLocale | en-US |
| Publisher | Your Name |
| PackageName | YourExtension for Command Palette |
| License | MIT |
| ShortDescription | A Command Palette extension that does X |
After answering all prompts, wingetcreate will create a PR against the winget-pkgs repository.
Step 6: Add the Command Palette Tag (CRITICAL)
This step is required for your extension to appear in Command Palette's browse experience.
After wingetcreate generates the manifest files, you must edit each .locale.*.yaml file to add the Command Palette tag.
In every locale YAML file (e.g., YourPublisher.YourExtension.locale.en-US.yaml), add:
Tags:
- windows-commandpalette-extension
Example of a complete locale file with the tag:
# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.6.0.schema.json
PackageIdentifier: YourPublisher.YourExtension
PackageVersion: 0.0.1
PackageLocale: en-US
Publisher: Your Name
PackageName: YourExtension for Command Palette
License: MIT
ShortDescription: A Command Palette extension that does X
Tags:
- windows-commandpalette-extension
ManifestType: defaultLocale
ManifestVersion: 1.6.0
Without this tag, Command Palette will not discover your extension in its browse experience.
Step 7: Ensure WindowsAppSdk Dependency
Your WinGet manifest must declare a dependency on the Windows App SDK so it gets installed automatically. In the installer.yaml manifest file, add:
Dependencies:
PackageDependencies:
- PackageIdentifier: Microsoft.WindowsAppRuntime.1.7
MinimumVersion: 7001.632.252.0
Note: Update the version number to match the Windows App SDK version your project targets. Check your
.csprojfor theWindowsAppSDKpackage version.
Step 8: GitHub Actions Automation (Optional)
Automate your build, release, and WinGet submission process with GitHub Actions.
Release Workflow: .github/workflows/release-extension.yml
name: Release Extension
on:
push:
tags:
- 'v*'
permissions:
contents: write
env:
PROJECT_NAME: YourExtension
DOTNET_VERSION: '9.0.x'
jobs:
build:
strategy:
matrix:
arch: [x64, arm64]
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Install Inno Setup
run: choco install innosetup -y --no-progress
- name: Detect version
id: version
run: |
$tag = "${{ github.ref_name }}" -replace '^v', ''
echo "VERSION=$tag" >> $env:GITHUB_OUTPUT
- name: Publish
run: |
dotnet publish -c Release -r win-${{ matrix.arch }} -o publish --self-contained=false
- name: Create installer
run: |
& "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" `
/DMyAppVersion="${{ steps.version.outputs.VERSION }}" `
/DArchitecturesAllowed="${{ matrix.arch }}" `
setup-template.iss
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: installer-${{ matrix.arch }}
path: Installer/*.exe
release:
needs: build
runs-on: ubuntu-latest
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
files: artifacts/*.exe
generate_release_notes: true
winget-update:
needs: release
runs-on: windows-latest
steps:
- name: Detect version
id: version
run: |
$tag = "${{ github.ref_name }}" -replace '^v', ''
echo "VERSION=$tag" >> $env:GITHUB_OUTPUT
- name: Update WinGet manifest
run: |
$baseUrl = "https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}"
wingetcreate update YourPublisher.YourExtension `
--version ${{ steps.version.outputs.VERSION }} `
--urls "$baseUrl/${{ env.PROJECT_NAME }}_${{ steps.version.outputs.VERSION }}_x64.exe" "$baseUrl/${{ env.PROJECT_NAME }}_${{ steps.version.outputs.VERSION }}_arm64.exe" `
--submit `
--token ${{ secrets.WINGET_PAT }}
Required Secrets
| Secret | Description |
|---|---|
WINGET_PAT |
GitHub Personal Access Token with public_repo scope, used by wingetcreate to submit PRs to microsoft/winget-pkgs |
How It Works
- Push a version tag (e.g.,
git tag v0.0.2 && git push origin v0.0.2) - Build job runs in parallel for x64 and ARM64, creating EXE installers
- Release job creates a GitHub Release and uploads the EXE files
- WinGet update job automatically submits an updated manifest to
winget-pkgs
Note: The
winget-updatejob useswingetcreate update(notnew) because it assumes you've already submitted your initial manifest manually. For the first submission, follow Steps 5–7 above.
Validation Checklist
Before submitting to WinGet, verify:
.csprojhas<WindowsPackageType>None</WindowsPackageType>set- CLSID in
setup-template.issmatches the[Guid("...")]in your main.csfile - Both x64 and ARM64 EXE installers build successfully
- Installer registers the COM server correctly (check
HKCU\Software\Classes\CLSID\{your-clsid}) - Extension appears in Command Palette after installing via EXE
- Extension is removed from Command Palette after uninstalling
- GitHub Release contains both EXE files with correct download URLs
- WinGet manifest includes
windows-commandpalette-extensiontag - WinGet manifest includes
WindowsAppRuntimedependency winget validatepasses on all manifest files
Updating Your Extension on WinGet
For subsequent releases:
wingetcreate update YourPublisher.YourExtension `
--version "0.0.2" `
--urls "<URL_TO_NEW_x64.exe>" "<URL_TO_NEW_arm64.exe>" `
--submit
Or simply push a new version tag if you've set up the GitHub Actions workflow above.
Troubleshooting
| Issue | Solution |
|---|---|
| Extension not appearing in CmdPal browse | Verify the windows-commandpalette-extension tag is in your locale YAML |
| COM registration fails | Check that the CLSID matches exactly and registry paths are correct |
wingetcreate validation errors |
Run winget validate --manifest <path> and fix reported issues |
| Installer doesn't run silently | Add /VERYSILENT /SUPPRESSMSGBOXES flags for silent install support |
| Missing WindowsAppSdk at runtime | Ensure the PackageDependencies section is in your installer manifest |