# How do I distribute a Go CLI to multiple platforms with GoReleaser and Homebrew?

> Let one tag-triggered workflow cross-compile and publish the GitHub Release while the brews: block updates the tap formula; the tap needs its own PAT.

- Asked: 2026-06-11
- Answered: 2026-06-12
- Asked by: Devrim
- Tags: ci-cd, go
- Source: https://www.muhammetsafak.com.tr/en/just-ask/multi-platform-go-cli-distribution-with-goreleaser-and-homebrew/
- Language: en-US
- Author: Muhammet Şafak

---
**Question:** We have a Go CLI tool (say, CommitBrief). I want both macOS (M1/M2/Intel) and Linux users to install it easily with `brew install`.

Using GoReleaser in GitHub Actions, how do I set up a CI/CD flow that builds binaries on every tag, uploads them to GitHub Releases, and auto-updates a private Homebrew Tap repo?


Short answer: this is exactly the problem GoReleaser solves — a `.goreleaser.yaml` plus a single tag-triggered workflow handles cross-compilation, the GitHub Release, and the Homebrew tap formula automatically.

All you really do is let go of the packaging chain; GoReleaser owns the rest.

1. **Define the build matrix in `.goreleaser.yaml`.** Target `darwin/arm64`, `darwin/amd64`, `linux/amd64`, `linux/arm64`. If you want, use `universal_binaries` to produce a single universal macOS artifact; the M1/M2/Intel split disappears for the user.
2. **Set up a tag-triggered GitHub Actions workflow.** With `on: push: tags: ['v*']`, run `goreleaser release`: it cross-compiles, builds archives + checksums, and publishes the GitHub Release.
3. **Auto-update the tap formula with a `brews:` block.** This block commits the formula to a separate `homebrew-tap` repo. Users then `brew install yourname/tap/cli`. What you need: a PAT scoped to push to the tap repo (the default `GITHUB_TOKEN` can't write to another repo, so a separate token is required).
4. **Stamp the version into the binary and validate the config in CI.** Use `ldflags` to stamp version/commit/date so `cli --version` reports correctly. Add `goreleaser check` to the pipeline — if the config is broken you catch it in the PR, not at release time.

**Bottom line:** push a tag and let GoReleaser do the rest — Releases + tap formula ship from one command, and manual packaging is gone. This is the de facto standard in the Go ecosystem; set up tag discipline and a separate tap token, and the flow runs itself. I cover the broader picture of packaging and distributing a CLI in the hub post; fit that distribution model onto this workflow.

## Related Reading

- [Packaging and Distributing a CLI Tool Written in Go](/en/blog/packaging-and-distributing-a-go-cli-tool/) — Blog
- [Should I run several dependent operations with errgroup so the first error cancels the rest?](https://www.muhammetsafak.com.tr/en/just-ask/should-i-run-several-dependent-operations-with-errgroup-so-the-first/) — Just Ask
- [How do I propagate context cancellation correctly through nested service calls?](https://www.muhammetsafak.com.tr/en/just-ask/how-do-i-propagate-context-cancellation-correctly-through-nested-service-calls/) — Just Ask
- [How do I detect and prevent goroutine leaks in a long-running Go service?](https://www.muhammetsafak.com.tr/en/just-ask/how-do-i-detect-and-prevent-goroutine-leaks-in-a-long/) — Just Ask
