Homebrew
Generate Homebrew formulae and push to tap repositories
Deprecation:
publish.homebrew(Homebrew Formula) is deprecated upstream in GoReleaser v2.16 and anodizer follows the same deprecation. The replacement is Homebrew Casks — the canonical Homebrew distribution channel for pre-compiled binaries.Quoting upstream: "Migrate to
homebrew_casks, which is the right tool for the job: it's how Homebrew expects pre-compiled binaries to be distributed, and it gets all the new features (completion generation, post-install hooks, and so on)."Anodizer still parses
publish.homebrewfor back-compat; aDEPRECATION:warning is emitted at config-load time. New projects should writehomebrew_casks:(top-level) orpublish.homebrew_cask:(per-crate) directly. See the GoReleaser migration guide for the side-by-side YAML diff.
Anodizer generates Ruby Homebrew formulae with multi-platform support and pushes them to your tap repository.
Tap vs. homebrew-core: this publisher pushes a formula to your own tap (
youruser/homebrew-tap). To bump a formula that has already been accepted intoHomebrew/homebrew-core, use the Homebrew Core publisher (homebrew_cores), which opens a fork-based pull request against the core repo instead of pushing to a tap you control.
Classification
| Group | Required (default) | Rollback | Token scope |
|---|---|---|---|
| Manager | false | re-clone tap, git revert HEAD --no-edit, push | GITHUB_TOKEN contents:write |
See Release resilience for the full classification table and the Submitter gate semantics.
The required: field
Default: false — a Homebrew push failure is logged but does not fail the release.
Set required: true to make the release exit non-zero if this publisher fails:
crates:
- name: myapp
publish:
homebrew:
repository:
owner: myorg
name: homebrew-tap
required: true
See Publish overview — the required: field for the full semantics.
Minimal config
crates:
- name: myapp
publish:
homebrew:
repository:
owner: myorg
name: homebrew-tapHomebrew config fields
| Field | Type | Default | Description |
|---|---|---|---|
repository.owner | string | — | GitHub owner of the tap repo |
repository.name | string | — | Tap repository name |
directory | string | Formula | Subdirectory within the tap repo for the .rb file |
description | string | Cargo [package].description | Formula description. Derived from Cargo.toml; set to override. |
license | string | Cargo [package].license | License identifier. Derived from Cargo.toml; set to override. |
install | string | auto | Custom install block (Ruby) |
test | string | none | Custom test block (Ruby) |
livecheck | object | skip | livecheck do … end stanza — version polling. See Livecheck. |
manpages | list | none | Man-page file paths to install via manN.install. See Auto completions and man pages. |
completions | object | none | Pre-built completion file paths (bash/zsh/fish). |
generate_completions_from_executable | object | none | Generate completions by running the installed binary at install time. |
Full config reference
crates:
- name: myapp
publish:
homebrew:
repository:
owner: myorg # required
name: homebrew-tap # required
token: "" # falls back to GITHUB_TOKEN
branch: "" # default: repo default branch (versioned <formula>-<version> when pull_request.enabled)
pull_request:
enabled: false
draft: false
base:
owner: ""
name: ""
branch: ""
directory: Formula # subdirectory in the tap
description: ""
license: ""
install: "" # custom Ruby install block
test: "" # custom Ruby test block
livecheck: # version polling; default skips (see Livecheck)
strategy: github_latest
url: stable
generate_completions_from_executable:
executable: myapp
args: [completion]
shells: [bash, zsh, fish]
manpages:
- myapp.1
skip_upload: false # bool or "auto" (skip prereleases)
cask: # per-crate cask config (same shape as homebrew_casks[])
update_existing_pr: falseHomebrew Cask config fields
Casks are configured under publish.homebrew.cask: (per-crate) or homebrew_casks: (top-level array). Both axes use the same HomebrewCaskConfig shape.
| Field | Type | Default | Description |
|---|---|---|---|
name | string | crate name | Cask name |
repository | object | — | Tap repo (owner, name, token, branch, pull_request) |
directory | string | Casks | Subdirectory in the tap for the .rb file |
app | string | none | macOS .app bundle name |
binaries | list | none | Binary stubs to install |
description | string | Cargo [package].description | Cask description. Derived from Cargo.toml; set to override. |
homepage | string | Cargo [package].homepage | Project homepage. Derived from Cargo.toml; set to override. |
skip_upload | bool or string | false | Skip publishing; true always skips, "auto" skips for prereleases |
update_existing_pr | bool or string | false | Force-push to an existing open PR branch instead of skipping. See Cask existing PR behavior. |
Authentication
| Variable | Description |
|---|---|
GITHUB_TOKEN | Token with push access to your tap repository (and pull_request:write for cask PR mode) |
The token can also be set via repository.token in the config.
Common gotchas
- Branch protection: if your tap repo has branch protection enabled, direct push will fail. Use a fork + PR workflow via
repository.pull_request. - Multiple platforms: anodizer auto-generates
on_macos/on_linux/on_intel/on_armstanzas from the build targets. If your build only produces one platform, the formula contains a singleurlblock instead of the multi-platform form. - Cask vs formula: formulae install from archives; casks install macOS
.appbundles. A crate can have both ifpublish.homebrew(formula) andpublish.homebrew.cask(or top-levelhomebrew_casks:) are both configured.
Republish / update behavior
Formula files are updated in-place on each release; re-cutting the same version overwrites the formula in the tap (prior commit stays in git history). The Manager group rollback reverts via git revert HEAD --no-edit + push.
Casks open a PR per version. Set update_existing_pr: true on the cask config to force-push to an existing open PR rather than opening a duplicate — full detail in the next section.
Cask existing PR behavior
When gh pr create reports a PR for the same head branch already exists,
Anodizer's default is to skip and emit a warning:
homebrew cask: PR for 'owner:myapp-cask-1.2.3' already exists — skipping
(set update_existing_pr: true to update the PR in place)
Setting update_existing_pr: true force-pushes the updated cask file to the
existing branch using --force-with-lease, so the open PR picks up the new
content without creating a duplicate:
# per-crate cask
publish:
homebrew:
cask:
update_existing_pr: true
# top-level homebrew_casks array
homebrew_casks:
- name: myapp
update_existing_pr: trueLivecheck
By default the formula emits livecheck do skip "Auto-generated on release." end
— a binary tap's archive URL and SHA are rewritten on every release, so there is
nothing stable for brew livecheck to poll. Set a strategy (and optionally
url/regex) to opt into active version detection instead. anodizer tags
releases on GitHub, so github_latest against :stable is the right pairing:
crates:
- name: myapp
publish:
homebrew:
livecheck:
strategy: github_latest
url: stable
renders into the formula:
livecheck do
url :stable
strategy :github_latest
end
url accepts a Ruby symbol shorthand (stable / head / homepage →
url :stable) or a literal URL string. regex is raw Ruby (e.g.
%r{v(\d+\.\d+)}i) for page_match-style strategies. Setting skip: false
without any of strategy/url/regex falls back to skip with a warning —
an empty livecheck do … end is invalid.
Auto completions and man pages
The formula installs shell completions and man pages without raw Ruby in the install block:
generate_completions_from_executablerenders the modern homebrew-core idiomgenerate_completions_from_executable(bin/"<exe>", ...), calling the installed binary at install time to emit its own completions. Preferred for a CLI that can print its completions (ripgrep/fd/bat all use this form).manpagesrenders oneman1.install "<path>"line per entry (a path ending in.Nfor N in 1–8 routes to the matchingmanNsection).completionsinstalls pre-built completion files when the archive ships ready-made ones (bash_completion.install/zsh_completion.install/fish_completion.install).
homebrew:
generate_completions_from_executable:
executable: myapp
args: [completion]
shells: [bash, zsh, fish]
manpages:
- myapp.1The test do block
Every generated formula carries a test do block (brew test runs it). When
test: is unset, anodizer emits a sensible default that invokes the installed
binary; set test: to supply your own Ruby assertions:
homebrew:
test: |
assert_match "myapp #{version}", shell_output("#{bin}/myapp --version")Dual-license rendering
When the resolved license is a compound SPDX expression (MIT OR Apache-2.0),
anodizer renders Homebrew's any_of license form rather than a single
license "MIT" — so the formula declares both licenses the way Homebrew audits
expect. A single-license crate renders the plain license "<spdx>" line.
Generated formula
Anodizer generates a formula with:
- Multi-platform download URLs (
on_macos,on_linux,on_intel,on_arm) - SHA-256 checksums for each archive
- Automatic binary installation
- Package name normalization (underscores → hyphens)
Full example
publish:
homebrew:
repository:
owner: myorg
name: homebrew-tap
directory: Formula
description: "A fast CLI tool"
license: MIT
install: |
bin.install "myapp"
test: |
system "#{bin}/myapp", "--version"
livecheck:
strategy: github_latest
url: stableArtifact eligibility
Formula: anodizer folds only the archives Homebrew can install into the
formula: genuine macOS targets (*-apple-darwin, darwin-universal) plus the
Linux archives it supports. Apple non-macOS targets (aarch64-apple-ios,
*-tvos, *-watchos) are excluded — they never appear in the generated
formula. If the build produces no eligible archive, anodizer fails the
release rather than emitting an empty formula. See
Artifact eligibility for how
this compares to the other install aggregators.
Cask: the embedded same-tap cask (cask:/homebrew_casks:) is
macOS-only — anodizer generates it only when the build produces a genuine
macOS artifact (*-apple-darwin), never from a Linux archive, matching the
standalone Homebrew Casks publisher.