Skip to content

Homebrew

Distribute a Go CLI through a Homebrew tap. Pilum cross-compiles the binary for four platforms, packages and checksums the archives, generates the formula and pushes it to your tap repository.

Pilum does not create GitHub Releases or upload the archives. The formula points at <project_url>/releases/download/<tag>/..., so the archives must be uploaded to that release before users run brew install (see Release Workflow).

Prerequisites

  1. Go installed (the build is a Go cross-compile)
  2. git installed
  3. A Homebrew tap repository, e.g. my-org/homebrew-tap
  4. A GitHub token that can push to the tap repository

The gh CLI is not required. Pilum clones and pushes the tap with plain git over HTTPS, using the token from the environment variable named in homebrew.token_env.

Service Configuration

name: mycli
type: homebrew
description: My CLI tool
license: MIT
build:
language: go
version_var: github.com/my-org/mycli/cmd.version
homebrew:
project_url: https://github.com/my-org/mycli
tap_url: https://github.com/my-org/homebrew-tap
token_env: GH_TOKEN

Use type: homebrew (or provider: homebrew).

Required Fields

FieldDescription
nameBinary name and formula name
descriptionFormula desc
licenseSPDX license identifier (MIT, Apache-2.0, …)
homebrew.project_urlRepository URL where the release archives are hosted, e.g. https://github.com/my-org/mycli
homebrew.tap_urlHTTPS URL of the tap repository
homebrew.token_envName of the environment variable holding a token that can push to the tap

Build Settings

The build is a fixed Go cross-compile. These build keys apply:

FieldTypeDescription
build.version_varstringGo variable to set to the tag, via -ldflags "-X <version_var>=<tag>"

build.cmd, build.flags, build.output and build.platforms are ignored by this recipe. project is not used. build.env_vars are exported into the step’s environment (useful for GOFLAGS or GOPRIVATE), but CGO_ENABLED is always 0.

The build runs go build . from the directory you run pilum in, so the main package must be there (normally the repository root). Each binary is built with -ldflags="-s -w".

Deployment Steps

The Homebrew recipe runs:

  1. build binaries [build] — go build for darwin/amd64, darwin/arm64, linux/amd64 and linux/arm64 into dist/<name>_<tag>_<os>_<arch>.
  2. create archives [build] — Packs each binary, renamed to <name>, into dist/<name>_<tag>_<os>_<arch>.tar.gz.
  3. generate checksums [build] — shasum -a 256 of the archives into dist/checksums.txt.
  4. update formula [deploy] — Writes dist/<name>.rb using the checksums.
  5. push to tap [deploy] — Clones the tap, copies the formula to Formula/<name>.rb, and commits and pushes as pilum[bot] with the message Update <name> to <tag>.

Always pass --tag. The tag is part of every file name and of the release URL; without it the names come out empty.

Generated Formula

For --tag=v1.0.0, Pilum writes dist/mycli.rb:

class Mycli < Formula
desc "My CLI tool"
homepage "https://github.com/my-org/mycli"
version "1.0.0"
license "MIT"
on_macos do
if Hardware::CPU.arm?
url "https://github.com/my-org/mycli/releases/download/v1.0.0/mycli_v1.0.0_darwin_arm64.tar.gz"
sha256 "..."
else
url "https://github.com/my-org/mycli/releases/download/v1.0.0/mycli_v1.0.0_darwin_amd64.tar.gz"
sha256 "..."
end
end
on_linux do
if Hardware::CPU.arm?
url "https://github.com/my-org/mycli/releases/download/v1.0.0/mycli_v1.0.0_linux_arm64.tar.gz"
sha256 "..."
else
url "https://github.com/my-org/mycli/releases/download/v1.0.0/mycli_v1.0.0_linux_amd64.tar.gz"
sha256 "..."
end
end
def install
bin.install "mycli"
end
test do
system "#{bin}/mycli", "--version"
end
end

version is the tag without a leading v. The release download path uses the tag as given.

Release Workflow

Split the pipeline around the release upload. Pilum’s own release workflow follows this order:

Terminal window
# 1. Build, archive and checksum (build-tagged steps only)
pilum build mycli --tag=v1.0.0
# 2. Create the GitHub Release for v1.0.0 and upload dist/*.tar.gz and dist/checksums.txt
gh release create v1.0.0 dist/*.tar.gz dist/checksums.txt --generate-notes
# 3. Generate the formula and push it to the tap (deploy-tagged steps only)
GH_TOKEN=... pilum deploy mycli --only-tags=deploy --tag=v1.0.0

Step 3 reads dist/checksums.txt, so it must run in the same checkout as step 1. A plain pilum deploy --tag=v1.0.0 runs all five steps in one go, which suits a release that is created before Pilum runs, but users cannot install until the archives are uploaded.

GitHub Token

The token needs write access to the tap repository:

  • Classic token: repo scope (or public_repo for a public tap).
  • Fine-grained token: Contents read/write on the tap repository.
Terminal window
export GH_TOKEN="ghp_xxxxx"

In GitHub Actions the default GITHUB_TOKEN cannot push to another repository, so store a separate token as a secret and expose it under the name set in token_env.

Example Commands

Terminal window
# Build binaries, archives and checksums only
pilum build mycli --tag=v1.0.0
# Full pipeline: build, formula, push to tap
pilum deploy mycli --tag=v1.0.0
# Preview commands
pilum dry-run mycli --tag=v1.0.0

Multi-Platform Builds

PlatformArchitectureArchive
macOSIntel (amd64)mycli_v1.0.0_darwin_amd64.tar.gz
macOSApple Silicon (arm64)mycli_v1.0.0_darwin_arm64.tar.gz
Linuxx86_64 (amd64)mycli_v1.0.0_linux_amd64.tar.gz
Linuxarm64mycli_v1.0.0_linux_arm64.tar.gz

The platform list is fixed.

Troubleshooting

Formula push fails

Error: GH_TOKEN environment variable is not set

Export the variable named in homebrew.token_env. If it is set but the push is rejected, the token lacks write access to the tap. tap_url must be an https:// URL, because the token is inserted into it for the clone.

brew install returns 404

The formula points at a release asset that does not exist. Upload dist/*.tar.gz to the GitHub Release whose tag matches --tag exactly.

Checksum mismatch

The archives in the release differ from the ones the formula was generated from, usually because they were rebuilt after update formula ran. Upload the archives and run the deploy steps from the same dist/ directory.

Binary not found

After pilum build, dist/ should contain the four .tar.gz archives and checksums.txt:

Terminal window
ls dist/
# checksums.txt
# mycli_v1.0.0_darwin_amd64.tar.gz
# mycli_v1.0.0_darwin_arm64.tar.gz
# mycli_v1.0.0_linux_amd64.tar.gz
# mycli_v1.0.0_linux_arm64.tar.gz

If the build step failed, check that the main package is in the directory you ran pilum from.

Installation by Users

Once the release assets are uploaded and the formula is in the tap:

Terminal window
brew tap my-org/tap
brew install mycli

Or in one command:

Terminal window
brew install my-org/tap/mycli

Next Steps