Skip to content

Add MSI installers for Windows and offer them through winget - #4948

Merged
junegunn merged 17 commits into
masterfrom
winget-msi
Nov 6, 2026
Merged

junegunn merged 17 commits into
masterfrom
winget-msi

Conversation

@junegunn

@junegunn junegunn commented Oct 29, 2026 •

Copy link
Copy Markdown
Owner

Based on @meop's branch from #4943, with fixes on top.

Problem

winget installs fzf as a portable package, so it runs fzf.exe through a symlink in %LOCALAPPDATA%\Microsoft\WinGet\Links. A non-admin user created that link, and an administrator's SSH session refuses to follow it (error 448, "The path cannot be traversed because it contains an untrusted mount point").

Changes

  • Build .msi installers for amd64 and arm64 with WiX 5 on a Windows runner (@meop)
    • The MSI installs fzf.exe under Program Files and adds it to the system PATH
  • Add the MSIs to the winget manifest (@meop)
  • goreleaser creates a draft release. The msi job uploads the MSIs and then publishes it.
    • Previously, winget.yml could start before the MSIs existed and leave them out of the manifest
    • If the msi job fails, the release stays a draft
    • replace_existing_draft keeps a retried release job from creating a second draft
    • The msi job fails if the release is already published, so a re-run cannot replace published MSIs
  • Only one approval per release, as before
    • The msi job publishes with GITHUB_TOKEN, so it does not need the release environment
    • A release published with GITHUB_TOKEN does not trigger other workflows, so release.yml calls winget.yml directly through workflow_call
    • winget.yml runs with a read-only token
  • Add the MSIs to fzf_<ver>_checksums.txt
  • Skip the MSI build for versions with a suffix such as -rc1, which MSI does not support
  • Remove make release. It would push master while the release was still a draft.
  • Update RELEASE.md for the draft flow

Testing

Not tested:

  • Retrying the release job over an existing draft
  • The actual winget submission with the MSIs
  • The guard against re-running the msi job after publishing

Close #4943

🤖 Generated with Claude Code

meop and others added 6 commits October 26, 2026 20:26
winget currently only has a `portable` installer to offer for fzf, which
installs via a symlink into the WinGet Links folder. That link is created
by whoever runs winget, normally a non-elevated terminal, and Windows will
not let an elevated process follow a link that a less-privileged process
created ("The path cannot be traversed because it contains an untrusted
mount point"). An administrator's SSH session is elevated, so fzf, and the
shell integration that runs it at startup, fails there.

Build a real per-machine .msi with the WiX dotnet-tool CLI (this is a Go
project, so cargo-wix doesn't apply) from the goreleaser-built fzf.exe, for
amd64 and arm64 (WiX's -arch has no armv7 target). It installs to Program
Files with its own machine PATH entry, so there is no link. WiX only runs
on Windows, so the release job hands the Windows binaries to a new msi job
on windows-latest. The MSIs are built in both workflow modes, so a
workflow_dispatch validation run checks them, and uploaded alongside the
.zip on a tag push; the winget-releaser installers-regex also matches .msi.
WiX is pinned to 5.0.2, the last release that does not require accepting
its Open Source Maintenance Fee EULA.
- goreleaser creates a draft release
- msi job publishes it after uploading the MSIs
- Publishing triggers winget.yml, which previously ran before the MSIs
  existed and left them out of the manifest
- If the msi job fails, the release stays a draft
- MSI versions cannot have a suffix such as -rc1, so wix build fails
- Release is still published
@github-actions github-actions Bot added docs Documentation ci CI/GitHub Actions build Build system labels Oct 29, 2026
- msi job used RELEASE_PAT only so that publishing would trigger
  winget.yml, which required approving the release environment again
- Publish with GITHUB_TOKEN instead and call winget.yml directly
- Skip winget for prerelease versions, as the released event did
@junegunn
junegunn requested a balanced review from Copilot October 29, 2026 12:46

This comment was marked as off-topic.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Approval should be deferred because the release path’s draft publication, Windows MSI and checksum uploads, and post-publish WinGet workflow remain unverified in an end-to-end real-tag run.

No open issues 🎉

Review effort: Lite

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Release retries can create duplicate drafts and publish an incomplete one, while the winget job exposes a write-capable repository token to an unpinned dependency.

2 open issues

Review effort: Max

Comment thread .github/workflows/release.yml Resolved
Comment thread .goreleaser.yml Resolved
- winget-releaser passes github.token to cargo-bins/cargo-binstall@main
- Token had contents: write from release.yml, and repository default
  (write) when run by hand
- goreleaser looks up the release by tag, which does not return drafts
- A retry created a second draft for the same tag, and msi job could
  publish either one

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

An unpublished, optional concern about the WiX usage command’s working-directory assumptions needs human judgment on its relevance to approval.

No open issues 🎉

2 resolved since last review

Review effort: Max

@meop meop left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed the diff and it looks good to me. Nothing blocking.

I also ran the real flow from tag pushes on my fork, since the PR lists it as untested, with Claude Code doing the runs: v0.99.11, v0.99.12, v0.99.13-rc1.

  • Draft and publish: goreleaser's draft ends up published and becomes latest over the older releases.
  • Checksums: both MSIs are in the file and the downloads match. Re-running the msi job doesn't duplicate the lines.
  • winget job: called through workflow_call, it finds both zips and both MSIs. Its token is read-only (a write attempt got a 403).
  • -rc1: published as a prerelease with no MSIs, winget skipped, latest unchanged.
  • MSIs, x64 and arm64: installing over the previous version leaves one Installed apps entry and one PATH entry. fzf.exe is the right architecture and runs from PATH. Uninstall removes the entry and folder, and restores PATH exactly.

How that differs from your release:

  • goreleaser ran on Ubuntu with fewer targets and no notarization, using GITHUB_TOKEN instead of RELEASE_PAT.
  • A stub replaced winget-releaser. It does the same release lookup and regex, but nothing was submitted, and Komac wasn't run.
  • I didn't test retrying the release job over an existing draft (replace_existing_draft).
  • The installs ran elevated on the runner, so no UAC. My earlier test on #4943 covered UAC on x64 only.

Small things, your call:

  1. wix/main.wxs: the usage comment says wix build main.wxs, but Source="LICENSE" looks like it only resolves from the repo root, which is where CI runs it. wix build wix/main.wxs would match.
  2. winget.yml: the regex still lists armv7, which has no Windows build now.
  3. CHANGELOG.md: existing portable installs stay portable on winget upgrade, so those users need to uninstall and reinstall once to get the fix. That may be worth a line.
  4. Re-running the msi job also re-runs the winget job, since it depends on msi. After a successful release, that would try to submit to winget again.

- Source="LICENSE" resolves from repository root, where CI runs it
@junegunn

junegunn commented Nov 4, 2026

Copy link
Copy Markdown
Owner Author

Thanks, @meop. I've addressed your comments in the following commits.

@junegunn
junegunn requested a review from meop November 4, 2026 12:04
- Release workflow replaced it
- With draft releases, it would push master while the release was still
  a draft, so install scripts would point at assets not yet public
- Re-run would replace published MSIs with new builds, whose hashes
  differ from a winget manifest that may already be merged
- Also stops winget job from submitting again

@meop meop left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, the changes look good to me.

@junegunn
junegunn merged commit 96028e2 into master Nov 6, 2026
5 of 6 checks passed
@junegunn

junegunn commented Nov 6, 2026

Copy link
Copy Markdown
Owner Author

Thanks for the review. I'll release a new version this weekend and see how it goes.

@meop

meop commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

Thank you for working on this so quickly.

@junegunn

junegunn commented Nov 7, 2026

Copy link
Copy Markdown
Owner Author

Please take a look: https://github.com/junegunn/fzf/releases/tag/v0.75.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

build Build system ci CI/GitHub Actions docs Documentation windows Windows

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Offer an MSI installer on winget so fzf works in admin SSH sessions on Windows

3 participants