Windows users currently have Scoop and the release archives. A winget package would make winget install utkuozdemir.pv-migrate work, which is the default package manager on Windows now. Windows is a small share of the downloads (about 2 percent of the release archive downloads as of 2026-09), so this is low priority, but it is cheap once the pieces below are in place.
Reference
nvidia_gpu_exporter already publishes to winget from goreleaser, so copy from there:
- The
winget: section of https://github.com/utkuozdemir/nvidia_gpu_exporter/blob/main/.goreleaser.yml. goreleaser writes the manifests (installer, locale, version) from the Windows zip archive and opens the pull request against microsoft/winget-pkgs itself. For pv-migrate: package_identifier: utkuozdemir.pv-migrate, publisher: utkuozdemir, license Apache-2.0, ids pointing at the archive id of the Windows zip, release_notes: "{{ .Changelog }}", a branch per version in the fork, pull_request.enabled: true with microsoft/winget-pkgs as the base.
- The release workflow needs nothing new, the publisher runs inside the existing goreleaser step.
Token
This is the part that is easy to get wrong.
- The winget publisher pushes to a fork of
microsoft/winget-pkgs and opens a pull request against Microsoft's repository. A fine-grained token cannot do that: it gets write access only to repositories the owner controls, and is read-only on everyone else's. The token has to be a classic PAT.
- Scopes:
repo (or at least public_repo) to push the branch and open the pull request, plus workflow. The workflow scope became necessary in 2026-09 when upstream added a workflow file to the repository, since goreleaser first syncs the fork with upstream and that sync then touches a workflow file. Without it the sync fails with a 422, goreleaser logs the error and still opens the pull request from the stale fork, so it works but the fork drifts. The exporter's token ("goreleaser nvidia_gpu_exporter" in the GitHub settings) has repo and workflow today.
- Keep pv-migrate's existing fine-grained
PRIVATE_ACCESS_TOKEN for the Homebrew tap and the Scoop bucket, where it is the tighter choice, and give the winget publisher its own environment variable in the goreleaser config (e.g. WINGET_TOKEN) backed by a second secret in the release environment. That way the broader classic token is used for the one job that needs it.
- The fork
utkuozdemir/winget-pkgs already exists from the exporter. One fork serves both projects, branches are named per package and version.
Other things to know
- The Microsoft CLA was agreed once for the exporter's first pull request, and it covers every later pull request from the same account, so no action there.
- The first pull request for a new package gets a closer look from the winget moderators than a version bump does, and the validation bot comments on the pull request. Nothing to do on our side except wait, and answer if a moderator asks.
- Once a maintainer approves, the pull requests merge on their own. No action per release.
- If the release fails at the winget step, everything else is already published. The fix is to correct the token or the manifest and re-run the workflow, never to publish the manifests manually.
- Prereleases:
skip_upload: auto keeps them out of winget, same as the other publishers.
- After the first published version, the README gets the badge the exporter has:
https://img.shields.io/winget/v/utkuozdemir.pv-migrate?style=flat-square, and the install guide a winget install utkuozdemir.pv-migrate section next to Scoop.
- The published version is
{{ .Version }} without the v, which is what winget wants. The archive names keep their v prefix, that does not matter for the manifest.
Windows users currently have Scoop and the release archives. A winget package would make
winget install utkuozdemir.pv-migratework, which is the default package manager on Windows now. Windows is a small share of the downloads (about 2 percent of the release archive downloads as of 2026-09), so this is low priority, but it is cheap once the pieces below are in place.Reference
nvidia_gpu_exporter already publishes to winget from goreleaser, so copy from there:
winget:section of https://github.com/utkuozdemir/nvidia_gpu_exporter/blob/main/.goreleaser.yml. goreleaser writes the manifests (installer, locale, version) from the Windows zip archive and opens the pull request againstmicrosoft/winget-pkgsitself. For pv-migrate:package_identifier: utkuozdemir.pv-migrate,publisher: utkuozdemir, license Apache-2.0,idspointing at the archive id of the Windows zip,release_notes: "{{ .Changelog }}", a branch per version in the fork,pull_request.enabled: truewithmicrosoft/winget-pkgsas the base.Token
This is the part that is easy to get wrong.
microsoft/winget-pkgsand opens a pull request against Microsoft's repository. A fine-grained token cannot do that: it gets write access only to repositories the owner controls, and is read-only on everyone else's. The token has to be a classic PAT.repo(or at leastpublic_repo) to push the branch and open the pull request, plusworkflow. Theworkflowscope became necessary in 2026-09 when upstream added a workflow file to the repository, since goreleaser first syncs the fork with upstream and that sync then touches a workflow file. Without it the sync fails with a 422, goreleaser logs the error and still opens the pull request from the stale fork, so it works but the fork drifts. The exporter's token ("goreleaser nvidia_gpu_exporter" in the GitHub settings) hasrepoandworkflowtoday.PRIVATE_ACCESS_TOKENfor the Homebrew tap and the Scoop bucket, where it is the tighter choice, and give the winget publisher its own environment variable in the goreleaser config (e.g.WINGET_TOKEN) backed by a second secret in thereleaseenvironment. That way the broader classic token is used for the one job that needs it.utkuozdemir/winget-pkgsalready exists from the exporter. One fork serves both projects, branches are named per package and version.Other things to know
skip_upload: autokeeps them out of winget, same as the other publishers.https://img.shields.io/winget/v/utkuozdemir.pv-migrate?style=flat-square, and the install guide awinget install utkuozdemir.pv-migratesection next to Scoop.{{ .Version }}without thev, which is what winget wants. The archive names keep theirvprefix, that does not matter for the manifest.