Publish Fanout through the Homebrew tap
Nobody has claimed this yet.
- Dominant language
- HTML
- Stars
- 4
- Forks
- 0
- Avg merge
- 4h 4m
- Merged PRs (30d)
- 57
Description
Context
Today the only published artifact is a single Docker image built for linux/amd64 (ghcr.io/labstack/fanout:latest, produced by .github/workflows/release.yml). Native binaries do not exist on the GitHub Releases page, even though:
- The product is positioned everywhere as "single binary."
- The homepage and
docs/install.mdboth advertisecurl -fsSL fanout.run/install.sh | sh— but noinstall.shexists insite/public/or anywhere in the repo. - The "Pre-built binary" section of
docs/install.mdpoints atgithub.com/labstack/fanout/releases— those releases currently have zero attached artifacts.
The gap between the marketing message and what someone actually gets when they follow the docs is the bug. This issue closes it.
CGO is required (duckdb-go/v2), but DuckDB ships precompiled native bindings for darwin-amd64, darwin-arm64, linux-amd64, linux-arm64, windows-amd64 — so we can produce binaries for those targets if we build on a matching native runner per target.
Who actually runs Fanout
- Production self-hosters (~70%): Linux VM or bare metal. amd64 dominant; arm64 growing fast (Graviton, Hetzner ARM, Ampere).
- Self-hosters / homelab (~20%): Single Linux box, often arm64 (Pi 5, Synology, mini-PC). Prefer native binary over container.
- Developers evaluating (~10%): macOS arm64 dominant, macOS amd64 fading. Install locally to kick the tires.
- Windows server: extremely rare for an OTel backend. Skip until asked.
Target platform matrix
| Tier | Platform | Distribution form |
|---|---|---|
| Must | linux/amd64 |
tarball, multi-arch Docker, install.sh, Homebrew |
| Must | linux/arm64 |
tarball, multi-arch Docker, install.sh, Homebrew |
| Should | darwin/arm64 |
tarball, install.sh, Homebrew |
| Should | darwin/amd64 |
tarball, install.sh, Homebrew |
| Defer | windows/amd64 |
wait for demand |
Distribution channels
- GitHub Releases — tarballs (
.tar.gz),SHA256SUMS, auto-generated changelog. - Docker (GHCR) — extend from amd64-only to
linux/amd64,linux/arm64viadocker buildx. Single multi-arch manifest. - install.sh — makes the docs honest. Detects OS+arch, downloads the right tarball, verifies checksum, installs to
/usr/local/bin/fanout. Lives atsite/public/install.shso Astro serves it fromhttps://fanout.run/install.sh. - Homebrew tap (
labstack/homebrew-tap) — goreleaser auto-publishes a formula on each tag.brew install labstack/tap/fanoutworks on macOS + Linux.
Implementation steps
1. Add goreleaser config
Create .goreleaser.yaml with:
before.hooks: buildweb/distviabun run build, copy intointernal/ui/dist/.- Four
buildsentries (linux_amd64,linux_arm64,darwin_amd64,darwin_arm64) withenv: ["CGO_ENABLED=1"],main: ./cmd/fanout,ldflags: ["-s", "-w", "-X main.version={{.Version}}"]. archives:.tar.gzwithname_template: fanout_{{.Version}}_{{.Os}}_{{.Arch}}. IncludeLICENSE,README.md.checksum:name_template: SHA256SUMS,sha256.changelog: filter outchore:anddocs:from public release notes.release:github,draft: false,prerelease: auto.brews:block pointing atlabstack/homebrew-tap.
2. Rework .github/workflows/release.yml
Current workflow is single-job Docker on ubuntu-24.04. Replace with three parallel jobs, all gated on the same tag:
-
binaries: matrix over four native runners:ubuntu-24.04→linux/amd64ubuntu-24.04-arm→linux/arm64macos-14(Apple Silicon) →darwin/arm64macos-13(Intel) →darwin/amd64
Each runner: install Go 1.26.1, install Bun,
bun installinweb/,goreleaser release --clean --split --single-target. -
docker: single job usingdocker buildxwithplatforms: linux/amd64,linux/arm64. Pushes multi-arch manifest toghcr.io/labstack/fanout:{tag,latest}. -
finalise: needs[binaries, docker]. Runsgoreleaser continue --mergeto upload all partial archives +SHA256SUMS+ Homebrew formula update as a single GitHub Release.
3. Wire version into the binary
- Add
var version = "dev"package var incmd/fanout/main.goand a-version/--versionflag that prints it and exits. - goreleaser's
-X main.version={{.Version}}populates it on release builds. justfilebuildtarget:-X main.version=$(git describe --tags --always --dirty)for local builds.
4. Write site/public/install.sh
Plain POSIX shell, runnable on macOS and Linux. Logic:
- Detect OS:
uname -s→darwin/linux; bail on anything else. - Detect arch:
uname -m→x86_64→amd64,aarch64/arm64→arm64. - Resolve latest release tag from
api.github.com/repos/labstack/fanout/releases/latest. - Download
fanout_{tag}_{os}_{arch}.tar.gzandSHA256SUMSfrom that release. - Verify checksum (
sha256sum -con linux,shasum -a 256 -con macOS). - Extract
fanout, install to/usr/local/bin/fanoutif writable else~/.local/bin/fanout(warn on PATH). - Print: "fanout {version} installed. Next: see https://fanout.run/docs/getting-started/".
Honour FANOUT_VERSION env var to pin a tag; FANOUT_PREFIX for custom install dir.
5. Update docs and site to match reality
| File | Change |
|---|---|
site/src/content/docs/docs/install.md |
Lead with "Quick install (curl)", then Docker, then "Pre-built binary" with per-platform table linking GitHub Releases assets, then Homebrew, then "From source." |
site/src/content/docs/docs/getting-started.md |
One-line note that the binary serves on 127.0.0.1:4317 by default while the Docker image listens on all interfaces. |
site/src/components/site/HowItWorks.astro |
Confirm step 1 ("Install") is truthful after this lands. |
6. Resolve the fanout up discrepancy
cmd/fanout/main.go has no subcommand library — invoking the binary runs the server. But the marketing mockup shows fanout up and fanout 0.4.2 installed.
Two paths:
- A — match the marketing copy: add a tiny CLI router using stdlib
flag.NewFlagSetper subcommand (no cobra).fanout upruns the server;fanout versionprints version. ~60 lines of change. - B — match the binary: change marketing/install copy to just
fanout(noup), and the install transcript to "Listening on :7520, :4317".
Recommendation: A. The up/version/migrate shape is what users expect from a server binary in 2026, and we already wrote the marketing around it.
7. Homebrew tap
Create a separate public repo labstack/homebrew-tap (one-time). Add a brews: block to .goreleaser.yaml:
name: fanouthomepage: https://fanout.rundescription: "Single-binary observability — OpenTelemetry ingest, fast UI, chat investigator."repository: { owner: labstack, name: homebrew-tap }test: |— runfanout --versionas formula smoke test.
Add a fine-scoped PAT (TAP_GITHUB_TOKEN) as a repo secret so goreleaser can push formula updates to the tap on each release.
8. CHANGELOG + release notes hygiene
goreleaser auto-generates a changelog from commit subjects. To make it useful:
- Keep conventional commit prefixes (
feat:,fix:,chore:,docs:,refactor:) — most recent commits already follow this. - Configure goreleaser to filter
chore:anddocs:out of public release notes. - No separate
CHANGELOG.md— GitHub release notes are the source of truth.
Files to create
.goreleaser.yaml
site/public/install.sh
Files to modify
.github/workflows/release.yml # 3-job split: binaries, docker, finalise
cmd/fanout/main.go # version var + minimal subcommand routing
justfile # local build embeds version via -X
site/src/content/docs/docs/install.md
site/src/content/docs/docs/getting-started.md
site/src/components/site/HowItWorks.astro
Separate repo to create
labstack/homebrew-tap (public, empty on creation)
Verification
End-to-end on a test tag (e.g. v0.0.0-test1):
git tag v0.0.0-test1 && git push --tags— workflow fires.- Workflow completes; GitHub Releases page shows:
- 4 tarballs (
fanout_v0.0.0-test1_{linux,darwin}_{amd64,arm64}.tar.gz) SHA256SUMS- Auto-generated release notes
- 4 tarballs (
docker manifest inspect ghcr.io/labstack/fanout:v0.0.0-test1shows bothlinux/amd64andlinux/arm64.- From a Linux amd64 box:
curl -fsSL https://fanout.run/install.sh | sh, verifyfanout --versionprintsv0.0.0-test1. - Repeat from macOS arm64.
brew tap labstack/tap && brew install fanout, verifyfanout --version.fanout upstarts the server, ingest works, UI loads.- Delete the test tag and test release after verification.
Out of scope (intentionally)
.deb/.rpmpackages — heavy lift, low payoff for v1; hand-rolled systemd unit in the binary tarball is good enough.- Windows binaries — DuckDB binding exists but Windows-server installs of an OTel backend are vanishingly rare. Add when someone asks.
- Auto-update mechanism in the binary — ops norm is "redownload, restart."
- Code signing (macOS notarization, Windows Authenticode) — defer until someone hits a Gatekeeper warning.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with .github/workflows/release.yml and the proposed .goreleaser.yaml, then inspect cmd/fanout/main.go, justfile, and the listed documentation and site files. Use the verification checklist with a test tag to validate release assets, multi-arch Docker output, install.sh, version reporting, and Homebrew installation. Done means all specified distribution paths work and the documentation matches them.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- bun, docker, github-actions, go, shell
- Domain
- build-system, cli, devops, documentation, release
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 30/100