·5 min read·#wsl-ui#tauri#winget#github-actions#ci-cd

Building WSL-UI: Publishing to winget

Building WSL-UI: Publishing to winget architecture diagram

WSL UI was already on the Microsoft Store. Job done, right? Not quite. I noticed the app wasn't showing up on any of the winget package search websites. Turns out, the Microsoft Store and the winget community repository are completely separate distribution channels.

Two Sources, One Package Manager

winget pulls packages from two sources:

  • msstore — The Microsoft Store. Apps submitted there appear when you search with --source msstore.
  • winget — The community repository at microsoft/winget-pkgs. This is the default source and what most winget websites and tools index.

Publishing to the Store doesn't automatically list your app in the community repo. If someone runs winget search "WSL UI" without specifying a source, they won't find it unless you've submitted a manifest to winget-pkgs.

building-wsl-ui-winget/winget-sources diagram
Click to expand
800 × 457px

The Manifest Format

A winget package manifest consists of three YAML files, stored in a specific directory structure:

manifests/ o/ OctasoftLtd/ WSLUI/ 0.16.2/ OctasoftLtd.WSLUI.yaml OctasoftLtd.WSLUI.installer.yaml OctasoftLtd.WSLUI.locale.en-US.yaml

The directory path follows the pattern: first letter of publisher / publisher / app name / version.

Version Manifest

The simplest file. It just declares what exists:

yaml
# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.9.0.schema.json PackageIdentifier: OctasoftLtd.WSLUI PackageVersion: 0.16.2 DefaultLocale: en-US ManifestType: version ManifestVersion: 1.9.0

Installer Manifest

This tells winget where to download your installers and how to verify them:

yaml
# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.9.0.schema.json PackageIdentifier: OctasoftLtd.WSLUI PackageVersion: 0.16.2 InstallerType: nullsoft UpgradeBehavior: uninstallPrevious ReleaseDate: 2026-02-09 Installers: - Architecture: x64 InstallerUrl: https://github.com/octasoft-ltd/wsl-ui/releases/download/v0.16.2/WSL.UI_0.16.2_x64-setup.exe InstallerSha256: 2B435C6A9657F13336ED57BE5BB1D9ECE56A0F565469E0F1D20018173971E984 - Architecture: arm64 InstallerUrl: https://github.com/octasoft-ltd/wsl-ui/releases/download/v0.16.2/WSL.UI_0.16.2_arm64-setup.exe InstallerSha256: 13D09805DEBE88B51170C70C1281A2B5B4EF7F726866D16AF930635138C0C9AC ManifestType: installer ManifestVersion: 1.9.0

Key details for Tauri apps:

  • InstallerType: nullsoft — Tauri produces NSIS installers, which winget calls nullsoft
  • SHA256 hashes — Must be uppercase hex. Compute with sha256sum or Get-FileHash
  • Multiple architectures — List each as a separate entry under Installers

I found the right format by looking at Clash Verge Rev, another Tauri app already in winget-pkgs.

Default Locale Manifest

Package metadata for the default language:

yaml
# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.9.0.schema.json PackageIdentifier: OctasoftLtd.WSLUI PackageVersion: 0.16.2 PackageLocale: en-US Publisher: Octasoft Ltd PublisherUrl: https://github.com/octasoft-ltd PackageName: WSL UI PackageUrl: https://github.com/octasoft-ltd/wsl-ui License: BUSL-1.1 LicenseUrl: https://github.com/octasoft-ltd/wsl-ui/blob/main/LICENSE ShortDescription: A lightweight desktop application to manage Windows Subsystem for Linux (WSL) distributions. Tags: - developer-tools - linux - virtualization - windows-subsystem-for-linux - wsl - wsl2 ReleaseNotesUrl: https://github.com/octasoft-ltd/wsl-ui/releases/tag/v0.16.2 ManifestType: defaultLocale ManifestVersion: 1.9.0

Submitting the PR

The process is straightforward: fork microsoft/winget-pkgs, add your manifest files, and open a PR.

Fork and Create the Files

You can do this manually through the GitHub UI, or use the gh CLI. I used the API approach to avoid cloning the massive repo (it has hundreds of thousands of files):

bash
# Fork the repo gh repo fork microsoft/winget-pkgs --clone=false # Create a branch via the API gh api repos/your-account/winget-pkgs/git/refs \ -X POST \ -f ref="refs/heads/OctasoftLtd.WSLUI-0.16.2" \ -f sha="$(gh api repos/your-account/winget-pkgs/git/ref/heads/master -q '.object.sha')"

Then push the manifest files and open a PR. The conventional PR title format is:

New package: OctasoftLtd.WSLUI version 0.16.2

For subsequent versions, use:

Update: OctasoftLtd.WSLUI version 0.17.0

The CLA Requirement

First-time contributors to winget-pkgs must sign Microsoft's Contributor License Agreement. A bot will comment on your PR asking you to agree. Reply with:

@microsoft-github-policy-service agree company="Your Company Name"

Or without the company parameter if you're contributing as an individual:

@microsoft-github-policy-service agree

This is a one-time requirement. Future PRs won't need it.

Validation Pipeline

After the CLA is signed, Microsoft's automated validation pipeline runs. It checks:

  • Schema compliance (correct YAML structure, required fields)
  • Installer URLs are accessible
  • SHA256 hashes match the actual downloads
  • No duplicate package identifiers
  • Manifest version consistency

If validation passes, a moderator reviews and merges. First submissions get extra scrutiny. Our PR was #340204.

Automating Future Releases

Submitting a manual PR for every release would get old fast. Microsoft provides wingetcreate — a CLI tool that generates and submits manifest updates automatically.

The CI/CD Job

I added a publish-winget job to the existing release-please pipeline. It runs after both architecture builds (x64 and arm64) complete:

yaml
publish-winget: needs: [release-please, build-release] if: ${{ needs.release-please.outputs.release_created }} runs-on: windows-latest steps: - name: Publish to winget shell: pwsh run: | # Install wingetcreate Invoke-WebRequest -Uri https://aka.ms/wingetcreate/latest -OutFile wingetcreate.exe # Extract version from tag $tag = "${{ needs.release-please.outputs.tag_name }}" $version = $tag -replace '^v', '' # Update manifest and submit PR to microsoft/winget-pkgs .\wingetcreate.exe update OctasoftLtd.WSLUI ` --version $version ` --urls ` "https://github.com/octasoft-ltd/wsl-ui/releases/download/${tag}/WSL.UI_${version}_x64-setup.exe" ` "https://github.com/octasoft-ltd/wsl-ui/releases/download/${tag}/WSL.UI_${version}_arm64-setup.exe" ` --submit ` --token "${{ secrets.WINGET_CREATE_PAT }}"
building-wsl-ui-winget/ci-pipeline diagram
Click to expand
1445 × 305px

How It Works

The wingetcreate update command:

  1. Downloads the existing manifest from winget-pkgs for OctasoftLtd.WSLUI
  2. Downloads both installer URLs you provide
  3. Computes SHA256 hashes automatically
  4. Updates the version, URLs, and hashes in the manifest YAML
  5. With --submit, forks winget-pkgs (if needed), pushes the updated manifest, and opens a PR

The --token flag provides the GitHub PAT that authorises the fork and PR creation.

Creating the PAT

The default GITHUB_TOKEN in GitHub Actions only has access to your own repository. Since wingetcreate needs to create PRs against microsoft/winget-pkgs, it needs a Personal Access Token.

  1. Go to GitHub Settings > Developer settings > Personal access tokens
  2. Create a classic token with the public_repo scope (this is the minimum required — it allows creating forks and PRs on public repos)
  3. Add it as a repository secret in your repo: Settings > Secrets and variables > Actions > New repository secret
  4. Name it WINGET_CREATE_PAT

Fine-grained tokens can also work, but classic tokens with public_repo are the simplest option and what Microsoft's documentation recommends.

The Full Release Flow

With this in place, here's what happens when a new version of WSL UI is released:

  1. Conventional commit lands on main
  2. Release-please creates a version bump PR
  3. PR merges, release-please creates a GitHub Release with the new tag
  4. build-release runs for both x64 and arm64 — builds Tauri, creates NSIS/MSI/MSIX/portable installers, uploads to the release
  5. publish-winget waits for both builds, then runs wingetcreate update which submits a manifest PR to winget-pkgs
  6. Microsoft's validation bot checks the manifest and a moderator merges it
  7. The app becomes installable via winget install OctasoftLtd.WSLUI

The whole pipeline is hands-off after the initial commit.

Installer URL Pattern

One thing to get right: the installer URLs must be predictable from the version number. For WSL UI, the Tauri build produces NSIS installers with this naming convention:

WSL.UI_{version}_{arch}-setup.exe

So the URL template is:

https://github.com/octasoft-ltd/wsl-ui/releases/download/v{version}/WSL.UI_{version}_{arch}-setup.exe

This is determined by the productName in tauri.conf.json and the Tauri build configuration. If your naming is different, adjust the URL pattern in the CI job accordingly.

Tips for Other Tauri Developers

  • Use nullsoft as InstallerType — That's what winget calls NSIS installers
  • Support multiple architectures — Tauri can build for both x64 and arm64 on Windows. List both in the manifest
  • Reference existing Tauri apps — Search winget-pkgs for apps using InstallerType: nullsoft to find format examples
  • Don't clone winget-pkgs — The repo is enormous. Use the GitHub API or wingetcreate instead
  • Schema version 1.9.0 — As of early 2026, this is the latest manifest schema version
  • Hashes must be uppercase — winget expects uppercase hex in InstallerSha256

Try It Yourself

Once the winget-pkgs PR is merged, WSL UI will be available through three channels:


This is Part 7 of a series on building WSL UI, a desktop app for managing WSL distributions on Windows.

← Back to all posts