Files
Olive/.github/skills/publish-extension/references/winget-publishing.md
T
2026-07-30 18:39:38 +02:00

13 KiB
Raw Blame History

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 AppId must 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 .csproj for the WindowsAppSDK package 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

  1. Push a version tag (e.g., git tag v0.0.2 && git push origin v0.0.2)
  2. Build job runs in parallel for x64 and ARM64, creating EXE installers
  3. Release job creates a GitHub Release and uploads the EXE files
  4. WinGet update job automatically submits an updated manifest to winget-pkgs

Note: The winget-update job uses wingetcreate update (not new) because it assumes you've already submitted your initial manifest manually. For the first submission, follow Steps 57 above.

Validation Checklist

Before submitting to WinGet, verify:

  • .csproj has <WindowsPackageType>None</WindowsPackageType> set
  • CLSID in setup-template.iss matches the [Guid("...")] in your main .cs file
  • 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-extension tag
  • WinGet manifest includes WindowsAppRuntime dependency
  • winget validate passes 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