Skip to main content

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.homebrew for back-compat; a DEPRECATION: warning is emitted at config-load time. New projects should write homebrew_casks: (top-level) or publish.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 into Homebrew/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

GroupRequired (default)RollbackToken scope
Managerfalsere-clone tap, git revert HEAD --no-edit, pushGITHUB_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-tap

Homebrew config fields

FieldTypeDefaultDescription
repository.ownerstringGitHub owner of the tap repo
repository.namestringTap repository name
directorystringFormulaSubdirectory within the tap repo for the .rb file
descriptionstringCargo [package].descriptionFormula description. Derived from Cargo.toml; set to override.
licensestringCargo [package].licenseLicense identifier. Derived from Cargo.toml; set to override.
installstringautoCustom install block (Ruby)
teststringnoneCustom test block (Ruby)
livecheckobjectskiplivecheck do … end stanza — version polling. See Livecheck.
manpageslistnoneMan-page file paths to install via manN.install. See Auto completions and man pages.
completionsobjectnonePre-built completion file paths (bash/zsh/fish).
generate_completions_from_executableobjectnoneGenerate 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: false

Homebrew 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.

FieldTypeDefaultDescription
namestringcrate nameCask name
repositoryobjectTap repo (owner, name, token, branch, pull_request)
directorystringCasksSubdirectory in the tap for the .rb file
appstringnonemacOS .app bundle name
binarieslistnoneBinary stubs to install
descriptionstringCargo [package].descriptionCask description. Derived from Cargo.toml; set to override.
homepagestringCargo [package].homepageProject homepage. Derived from Cargo.toml; set to override.
skip_uploadbool or stringfalseSkip publishing; true always skips, "auto" skips for prereleases
update_existing_prbool or stringfalseForce-push to an existing open PR branch instead of skipping. See Cask existing PR behavior.

Authentication

VariableDescription
GITHUB_TOKENToken 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_arm stanzas from the build targets. If your build only produces one platform, the formula contains a single url block instead of the multi-platform form.
  • Cask vs formula: formulae install from archives; casks install macOS .app bundles. A crate can have both if publish.homebrew (formula) and publish.homebrew.cask (or top-level homebrew_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: true

Livecheck

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 / homepageurl :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_executable renders the modern homebrew-core idiom generate_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).
  • manpages renders one man1.install "<path>" line per entry (a path ending in .N for N in 1–8 routes to the matching manN section).
  • completions installs 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.1

The 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: stable

Artifact 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.