Skip to content

chore: Migrate brews to homebrew_casks - #351

Merged
masutaka merged 3 commits into
mainfrom
migrate-brews-to-homebrew-casks
Aug 13, 2026
Merged

chore: Migrate brews to homebrew_casks#351
masutaka merged 3 commits into
mainfrom
migrate-brews-to-homebrew-casks

Conversation

@masutaka

@masutaka masutaka commented Aug 13, 2026

Copy link
Copy Markdown
Owner

Closes #283

Why now

Homebrew has not dropped Formulas — GoReleaser dropped brews.

brews was soft-deprecated in v2.10 and fully deprecated in v2.16.0 (2026-05-24). Since then brew.Pipe.Default() sets ctx.Deprecated merely because a brews section exists, and goreleaser check exits 2 on ctx.Deprecated. Our Makefile has release: deps-release release-check, so make release stops there. See the deprecation notice for GoReleaser's own migration guide.

The last successful release job ran on 2026-04-29 (v4.3.0), before v2.16.0 landed. The release workflow installs latest via goreleaser-action, so the next tag would have failed.

Verified locally against GoReleaser v2.17.1:

$ goreleaser check <old config>; echo $?
  • DEPRECATED:  brews  should not be used anymore
  ⨯ check failed    error=1 out of 1 configuration file(s) have issues
2

$ goreleaser check; echo $?
  • 1 configuration file(s) validated
0

The brews implementation itself still ships in v2.17.1 — GoReleaser only removes deprecated options on major versions, so releases would keep working until v3.0. Only goreleaser check fails. Migrating now rather than deferring to a v3.0 emergency.

What changed

homebrew_casks has no install or test block, which is what #283 flagged. Mapping:

Before (brews) After (homebrew_casks)
bin.install 'github-nippou' binary stanza (defaults to the cask name)
install: completion-generation Ruby generate_completions_from_executable (v2.15+)
test: no cask equivalent — dropped
license: "MIT" dropped; the cask pipe never reads it
hooks.post.install to strip quarantine (newly required)
url.verified to help brew audit

Two cask-specific notes:

  • Quarantine. Unlike Formulas, Casks tag their downloads with com.apple.quarantine. Our binaries are neither signed nor notarized, so without the xattr -dr postflight hook users hit "github-nippou is damaged and cannot be opened" — and generate_completions_from_executable cannot execute the binary either. GoReleaser's template guarantees postflight is emitted before it.
  • shells is explicit. shell_parameter_format: cobra defaults to including pwsh; pinning to bash/zsh/fish keeps parity with the old formula.

The Makefile change is separate and pre-existing: go install github.com/goreleaser/goreleaser@latest resolves to v1.26.2, since v2 lives under the /v2 module path. v1 understands neither archives.formats nor homebrew_casks, so local verification was impossible. CI is unaffected (goreleaser-action with install-only).

Generated cask

make dist output (dist/homebrew/Casks/github-nippou.rb, abridged):

cask "github-nippou" do
  version "4.3.0-next"

  on_macos do
    on_intel do
      sha256 "9d13598ada75991d9f8983879cded777669d7f6a49055db40ce4809cb6fb5c5b"
      url "https://github.com/masutaka/github-nippou/releases/download/v4.3.0/github-nippou_v#{version}_darwin_amd64.zip",
        verified: "github.com/masutaka/github-nippou/"
    end
    ...
  end

  binary "github-nippou"

  postflight do
    if OS.mac?
      system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "#{staged_path}/github-nippou"]
    end
  end
  generate_completions_from_executable "github-nippou", "completion",
    shell_parameter_format: :cobra,
    shells: [:bash, :zsh, :fish]
end

Linux keeps working

Worth stating explicitly, because "Casks are macOS-only" is a common belief and it is not accurate: Homebrew gates casks per artifact, not per cask. From Library/Homebrew/cask/artifact.rb#L42-L63:

MACOS_ONLY_ARTIFACTS = [
  ::Cask::Artifact::App,
  ::Cask::Artifact::Pkg,
  ::Cask::Artifact::Prefpane,
  ::Cask::Artifact::ScreenSaver,
  ...
].freeze

LINUX_ONLY_ARTIFACTS = [
  ::Cask::Artifact::AppImage,
].freeze

This cask uses Binary, PostflightBlock and GeneratedCompletion — none of them are in MACOS_ONLY_ARTIFACTS, so Cask#artifacts_supported_on_os?(:linux) returns true. Artifact::Binary is just a symlink plus chmod +x, with no OS-specific code, and brew install has no cask-wide OS gate. The existence of LINUX_ONLY_ARTIFACTS shows Homebrew supports Linux casks by design.

The reason casks feel macOS-only is that the homebrew/cask tap is almost entirely app and pkg artifacts — a property of its contents, not a limit of the mechanism.

GoReleaser therefore keeps emitting the on_linux block, and so does every cask in goreleaser/homebrew-tap and caarlos0/homebrew-tap. GoReleaser's own install docs list brew install --cask goreleaser/tap/goreleaser with no OS split. The quarantine hook is guarded by if OS.mac?, so it is skipped on Linux.

Not verified on an actual Linux box — this is from reading Homebrew's source and GoReleaser's own distribution.

Install instructions

README.md and README_ja.md now say brew install --cask masutaka/tap/github-nippou, matching masutaka/homebrew-tap#2.

--cask is not strictly required. load_formula_or_cask tries the formula first and falls through to the cask when Formulary.factory raises FormulaUnavailableError — which is exactly what happens once the formula is deleted from the tap. Existing muscle memory keeps working. Spelling it out just keeps both repos consistent, the way GoReleaser's own docs do.

The macOS-only wording is gone for the reason above: casks are gated per artifact, not per cask.

Known trade-off

Losing test do means the github-nippou version smoke test goes away. Casks have no equivalent.

Follow-up in masutaka/homebrew-tap (not in this PR)

The new cask lands at Casks/github-nippou.rb; the old Formula sits at the repo root as github-nippou.rb. After the first cask release:

  1. Add tap_migrations.json at the tap root:

    {
      "github-nippou": "github-nippou"
    }

    (same shape as goreleaser/homebrew-tap and caarlos0/homebrew-tap)

  2. rm github-nippou.rb

Order matters. The cask must exist first, because the migration branch is gated on new_tap.cask_tokens.intersect?(...). And the formula file must actually be deleted, since brew update detects the migration from the git diff.

No announcement needed, and users do not need to uninstall anything first. Homebrew handles formula→cask migration explicitly in migrate_tap_migration:

# For formulae migrated to cask: Auto-install cask or provide install instructions.
if new_tap.core_cask_tap? || new_tap.cask_tokens.intersect?([new_full_name, new_name])
  ...
  if new_tap.installed? && (HOMEBREW_PREFIX/"Caskroom").directory?
    ohai "brew unlink #{name}"
    system ... "unlink", name
    ohai "brew cleanup"
    system ... "cleanup"
    ohai "brew install --cask #{new_full_name}"
    system ... "install", "--cask", new_full_name
    ohai migration_message, <<~EOS
      The existing keg has been unlinked.
      Please uninstall the formula when convenient by running:
        brew uninstall --formula --force #{name}
    EOS

So brew update alone unlinks the old keg, cleans up, installs the cask, and then tells the user to run brew uninstall --formula --force github-nippou at their convenience. Homebrew produces that message itself. Where Caskroom does not exist yet, it prints the manual steps instead of auto-installing.

Verification

  • make release-check passes (exit 0; old config exits 2)
  • make dist generates the cask above
  • After the tap follow-up, on a machine with the old formula still installed: run brew update and confirm it auto-migrates (unlink → cleanup → install --cask). Do not uninstall first — that would bypass the path being tested.
  • Then check github-nippou version and tab completion, and run the brew uninstall --formula --force github-nippou that Homebrew suggests (completions needed fix: Move the quarantine hook ahead of completion generation #352 — see comment below)
  • Separately, a clean install: brew install masutaka/tap/github-nippou
  • On Linux if convenient, to confirm the on_linux block behaves as the source reading suggests

`github.com/goreleaser/goreleaser@latest` resolves to v1.26.2 because v2
lives under the `/v2` module path. v1 understands neither `archives.formats`
nor `homebrew_casks`, so `make release-check` and `make dist` were broken
on a machine without GoReleaser already installed.
`brews` became fully deprecated in GoReleaser v2.16, so `goreleaser check`
now exits 2 and takes `make release` down with it. Verified: the old config
exits 2 under v2.17.1, the new one exits 0.

- `install` is replaced by the default `binary` stanza plus
  `generate_completions_from_executable`
- Casks quarantine their downloads, so a postflight `xattr` hook is needed
  for the binary to run at all and for completion generation to work
- `test` and `license` have no cask equivalent, so they are dropped
- Spell out `--cask`, matching masutaka/homebrew-tap#2. Homebrew falls back
  to the cask on its own once the formula is gone, so this is for clarity
  rather than correctness
- Drop the macOS-only wording: Homebrew gates casks per artifact, and
  `binary` is not in `MACOS_ONLY_ARTIFACTS`
@masutaka
masutaka merged commit d9724fa into main Aug 13, 2026
8 checks passed
@masutaka
masutaka deleted the migrate-brews-to-homebrew-casks branch August 13, 2026 07:51
masutaka added a commit to masutaka/homebrew-tap that referenced this pull request Aug 13, 2026
masutaka/github-nippou#351 で brews を homebrew_casks へ移行したため、
その受け皿を tap 側に用意する。

- brew update は git diff から移行を検知するので Formula 削除が必須
- tap_migrations.json は Casks/github-nippou.rb の存在が前提
@masutaka

Copy link
Copy Markdown
Owner Author

Outcome, and a correction to this PR body

The migration itself worked. brew update auto-migrated exactly as described:

==> masutaka/tap/github-nippou has been migrated from a formula to a cask.
==> brew unlink github-nippou
==> brew cleanup
==> brew install --cask masutaka/tap/github-nippou
🍺  github-nippou was successfully installed!

Two things this PR body got wrong.

1. Completions were broken

Two bugs, fixed in #352: the quarantine hook fired after GeneratedCompletion (Homebrew sorts artifacts by class, so hooks.post is too late — it has to be pre), and args: [completion] duplicated the subcommand that shell_parameter_format: cobra already appends. Verified working on v4.3.2.

2. "--cask is not strictly required" — not true for upgrading users

This PR body claimed the tap-qualified name resolves to the cask on its own once the formula is deleted from the tap. That holds for a fresh install, but not for anyone migrating:

$ brew upgrade github-nippou
Warning: Treating github-nippou as a formula. For the cask, use masutaka/tap/github-nippou or specify the `--cask` flag.
Warning: masutaka/tap/github-nippou 4.3.0 already installed

load_formula_or_cask prioritises formulae, and Formulary.factory still resolves a formula whose keg is installed locally, even after the .rb is gone from the tap. So the cask is only reached once the old formula is actually uninstalled:

brew uninstall --formula --force github-nippou

Homebrew already prints that suggestion during migration, so nothing extra is needed from us — but the claim in the body was too broad. Spelling out --cask in the README was the right call after all.

Remaining unchecked

A clean install and a Linux check are still untested. Neither blocks anything.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Migrate brews section to homebrew_casks in .goreleaser.yaml

1 participant