Skip to content

release: fix the homebrew cask so it loads at all - #640

Merged
zzet merged 1 commit into
mainfrom
fix/homebrew-cask-preflight-stanzas
Aug 20, 2026
Merged

release: fix the homebrew cask so it loads at all#640
zzet merged 1 commit into
mainfrom
fix/homebrew-cask-preflight-stanzas

Conversation

@zzet

@zzet zzet commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Closes #639.

The bug

brew install / brew upgrade fail for every macOS user on both arches since v0.63.7:

Error: Cask 'gortex' definition is invalid: undefined method 'pre_install' for Cask 'gortex'

The cask generated by release.yml used pre_install / post_install. Those are formula DSL methods — a cask has no such stanzas (preflight / postflight are the equivalents). Brew evaluates the cask on every command that touches it, so install, upgrade and even brew info all fail. The v0.63.6 cask is clean; the breakage arrived with the daemon stop/restart hooks and shipped to the tap on the 20th.

Reproduced against the live tap file with a real brew, and the fixed cask loads cleanly.

Two more defects a plain rename would have left behind

The daemon probe would abort the install. The cask DSL's system_command is SystemCommand.run!must_succeed: true, which raises on a non-zero exit. gortex daemon status exits 1 exactly when no daemon is reachable, so next unless status.success? was unreachable: brew would have hard-failed the install on any machine with gortex installed but no daemon running. The probe now passes must_succeed: false (verified: the call returns a failed Result instead of raising).

preflight is the wrong hook. On upgrade, Cask::Upgrade runs old_cask_installer.start_upgradeuninstall_artifactsArtifact::Symlinked#uninstall_phaseunlink before new_cask_installer.install_artifacts, and the preflight block is a new-cask artifact. By the time it runs, $HOMEBREW_PREFIX/bin/gortex is already gone and File.executable?(bin) is false — so the "stop the daemon" half could never fire on the very path it was written for.

The fix

One postflight block does both halves. After the new binary is linked, probe for a live daemon and, only if one answers, run daemon restart — which stops the old process (blocking until it exits, releasing the store lock) before starting the new one, so any store migration still runs alone. Fresh installs and CI have no daemon answering and skip it. Unlike an uninstall_preflight on the old cask, this works when upgrading from any already-released version, including the broken 0.63.7.

The cask body moves out of the release.yml heredoc into .github/homebrew/gortex.rb.tmpl, rendered by scripts/render-cask.sh. Rendered output is byte-identical to the published cask apart from the hook block. The renderer also rejects a malformed sha256, a version with a leading v, and any placeholder that survived substitution.

Why it reached users, and what stops the next one

The file was valid Ruby and valid YAML, the Go suite can't see it, and the tap has no CI — so the first machine to evaluate the cask was a user's. Only a real brew loading the DSL catches an unknown stanza.

scripts/validate-cask.sh renders the template with dummy values and loads it through brew in a scratch tap. It runs:

  • on every PR touching the cask, renderer, validator or release workflow (.github/workflows/homebrew-cask.yml)
  • again in build-darwin, as the last gate before the release job pushes to the tap

Both are macOS-only — Homebrew on Linux cannot load casks at all.

Verified by neutering the fix: putting pre_install back in the template makes the validator fail with the exact error users reported, and exit 1.

Follow-up outside this repo

The tap still serves the broken 0.63.7 cask, so macOS installs stay broken until either zzet/homebrew-tap Casks/gortex.rb is patched by hand or a new release publishes over it. Until then the workaround is GORTEX_NO_BREW=1 with scripts/install.sh, or the tarball directly.

The generated cask used `pre_install` / `post_install`. Those are formula
DSL methods; a cask has no such stanzas, so brew rejected the file at load
time with

  Error: Cask 'gortex' definition is invalid: undefined method 'pre_install'

Every brew command that touches the cask parses it, so `brew install` and
`brew upgrade` both failed for every macOS user on both arches, starting
with v0.63.7. Closes #639.

Two further defects made a straight rename insufficient:

  * The cask DSL's `system_command` is `SystemCommand.run!`, which raises on
    a non-zero exit. `gortex daemon status` exits 1 exactly when no daemon
    is reachable, so `next unless status.success?` could never run — brew
    would have aborted the install outright on any machine without a live
    daemon. The probe now passes `must_succeed: false`.

  * `preflight` was the wrong hook for stopping the daemon. On upgrade brew
    unlinks the old cask's binary (`start_upgrade` -> `uninstall_artifacts`)
    before installing the new cask's artifacts, so by the time a preflight
    block runs there is no gortex on disk to ask. The stop half could never
    fire on the path it was written for.

`postflight` handles both halves instead: after the new binary is linked,
probe for a daemon and, only if one answers, `daemon restart` — which stops
the old process (blocking until it exits, releasing the store lock) before
starting the new one, so a store migration still runs alone. A fresh install
and CI have no daemon answering and skip it.

The cask body moves out of the release.yml heredoc into
.github/homebrew/gortex.rb.tmpl, rendered by scripts/render-cask.sh. That
makes it reviewable as Ruby, removes the shell-expansion hazards of an
unquoted heredoc, and — the point — lets a real brew load it before
publication. The renderer also refuses a malformed sha256, a version with a
leading "v", or a placeholder that survived substitution. Rendered output is
byte-identical to the published cask apart from the hook block.

Nothing could have caught this: the file was valid Ruby, valid YAML, and the
tap has no CI, so the first machine to evaluate the cask was a user's.
scripts/validate-cask.sh renders the template with dummy values and loads it
through a real brew. It runs on every PR that touches the cask
(.github/workflows/homebrew-cask.yml) and again in build-darwin as the last
gate before the release job pushes to the tap. Both are macOS-only —
Homebrew on Linux cannot load casks. Verified by reintroducing `pre_install`
in the template: the validator fails with the exact error users reported.
@zzet
zzet merged commit 5f9de46 into main Aug 20, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Homebrew cask is broken

1 participant