From 8af4d0b2df911bb115111ad943f83d386a02f549 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 19:04:05 +0000 Subject: [PATCH 1/2] fix(release): emit postflight_steps in the Homebrew cask Homebrew 7.0 deprecated the `postflight` stanza in favour of the declarative `postflight_steps`, so every `brew` command touching the cask printed Warning: Calling `postflight` is deprecated! Use `postflight_steps` instead. and Homebrew deprecations become hard errors a few releases later. The stanza is the cask's quarantine strip, which the unsigned binary needs to launch on macOS, so it cannot simply be dropped. GoReleaser generates `postflight` from `homebrew_casks.hooks.post.install` and cannot emit the `*_steps` stanzas yet (goreleaser/goreleaser#6870, PR #6873, milestone v2.19.0), so write the stanza directly through `custom_block` instead. Two details are load-bearing and documented next to the block: the Homebrew path token is escaped as `{{ "{{staged_path}}" }}` because GoReleaser runs the whole generated cask through its template engine as a final pass, and the steps DSL has no Ruby interpolation, so `run` takes the expanded token as an argument. Placement right after `cask "koc" do` is safe because Homebrew orders artifacts by class (Binary before PostflightSteps), not by file position. Verified by building GoReleaser from source and running a snapshot release: the generated cask carries `postflight_steps`, the literal `{{staged_path}}` token survives the template pass, and the file is valid Ruby. The cask now requires Homebrew >= 7.0, which README and AGENTS.md note. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0122vGrcATX7433WuQ91iLm9 --- .goreleaser.yaml | 43 ++++++++++++++++++++++++++++++++++++------- AGENTS.md | 18 ++++++++++++++++++ README.md | 5 ++++- 3 files changed, 58 insertions(+), 8 deletions(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 40236fd..72718e3 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -202,10 +202,39 @@ homebrew_casks: # The binary is unsigned (Go only ad-hoc-signs darwin/arm64 so it *runs*); # casks are quarantined on download, so strip the quarantine flag on install # or launch dies with "koc cannot be verified". - hooks: - post: - install: | - if OS.mac? - system_command "/usr/bin/xattr", - args: ["-dr", "com.apple.quarantine", "#{staged_path}/koc"] - end + # + # This is a raw `custom_block` rather than GoReleaser's `hooks.post.install` + # because that hook is emitted as Homebrew's `postflight do … end`, which + # Homebrew 7.0 deprecated in favour of the declarative `postflight_steps`: + # every `brew` command touching the cask printed + # Warning: Calling `postflight` is deprecated! Use `postflight_steps` instead. + # and Homebrew deprecations turn into errors a few releases later. GoReleaser + # cannot emit the `*_steps` stanzas yet (goreleaser/goreleaser#6870, PR #6873, + # milestone v2.19.0) — when that lands, move this back to + # `hooks.post.install_steps` and delete the block. + # + # Three things about the form below: + # - `custom_block` is injected right after `cask "koc" do`, so the stanza + # order in the generated file is unconventional. It is still correct: + # Homebrew orders artifacts by class (Binary, then PostflightSteps — see + # Cask::Artifact::AbstractArtifact.sort_order), never by file position, + # so this runs after the binary is staged and linked. + # - `{{ "{{staged_path}}" }}` renders as the literal `{{staged_path}}`. + # GoReleaser runs the whole generated cask through its template engine as + # a final pass, so the Homebrew token has to survive it escaped; Homebrew + # expands it in step args at install time (the steps DSL has no Ruby + # interpolation — that was the point of deprecating `postflight`). + # - `run` is the steps DSL's escape hatch for a command; the sandbox it + # runs under already grants write access to the caskroom, which is where + # `staged_path` lives, so no `writable_paths` is needed. + # + # This requires Homebrew >= 7.0: an older client raises on the unknown + # stanza rather than warning. `brew install` auto-updates first, so in + # practice only HOMEBREW_NO_AUTO_UPDATE=1 users can hit that. + custom_block: | + postflight_steps do + on_macos do + run "/usr/bin/xattr", + args: ["-dr", "com.apple.quarantine", "{{ "{{staged_path}}" }}/koc"] + end + end diff --git a/AGENTS.md b/AGENTS.md index 4ab31dc..325c596 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -540,6 +540,24 @@ completions`**, run in its own secret-free step before GoReleaser. Locally, run `make completions` before `goreleaser release`/`build` or the archives will be missing `completions/`. +**The cask's quarantine step is a `custom_block`, not a GoReleaser hook.** The +binary is unsigned, so the cask has to strip `com.apple.quarantine` from the +staged `koc` or macOS refuses to launch it. GoReleaser emits +`homebrew_casks.hooks.post.install` as Homebrew's `postflight do … end`, which +Homebrew 7.0 deprecated in favour of the declarative `postflight_steps` — every +`brew` command touching the cask printed a deprecation warning, and Homebrew +deprecations become errors a few releases later. GoReleaser cannot emit the +`*_steps` stanzas yet (goreleaser/goreleaser#6870, PR #6873, milestone v2.19.0), +so `.goreleaser.yaml` writes the stanza itself through `custom_block`. Three +things to keep in mind when touching it, all spelled out next to the block: the +Homebrew path token has to be escaped as `{{ "{{staged_path}}" }}` because +GoReleaser runs the whole generated cask through its own template engine as a +final pass; `custom_block` lands right after `cask "koc" do`, which is fine +because Homebrew orders artifacts by class rather than by file position; and the +cask now requires Homebrew >= 7.0, since an older client raises on the unknown +stanza instead of warning. When GoReleaser ships `hooks.post.install_steps`, +move back to it and delete the block. + **Builds are byte-reproducible, and it takes more than one setting.** `builds: mod_timestamp: {{ .CommitTimestamp }}` fixes the binary, but a tar/zip records every *member's* mtime, so `LICENSE`, `README.md` and the generated completions diff --git a/README.md b/README.md index 2c33335..23a50a0 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,10 @@ brew install ftarasenko/tap/koc No `--cask` flag is needed — nothing else in the tap shares the name. The binary is unsigned, so the cask strips the macOS quarantine flag on install; on Apple -Silicon Go already ad-hoc-signs the binary so it runs. +Silicon Go already ad-hoc-signs the binary so it runs. That step is written with +Homebrew's `postflight_steps` stanza, so the cask needs **Homebrew 7.0 or +newer** — `brew install` auto-updates itself before it reads the cask, so only +`HOMEBREW_NO_AUTO_UPDATE=1` users have to update by hand first. ### Shell completion From 1e1ce277e2f104e668c29f189e9dab5418fa4bec Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 19:20:27 +0000 Subject: [PATCH 2/2] fix(release): keep the cask installable on pre-6.0.13 Homebrew MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit moved the quarantine strip to `postflight_steps` unconditionally, which silences the deprecation warning but raises on any Homebrew that predates the stanza. Pick the spelling at load time instead: `postflight_steps` where it works, the original `postflight` block below that. The cask now installs on every Homebrew and warns on none. Three versions matter, and they are three releases apart, which is why the obvious probe is wrong: 6.0.0 the postflight_steps stanza lands 6.0.13 the steps DSL gains the run/on_macos steps the block calls 6.0.16 postflight is deprecated So the probe is `Homebrew::InstallSteps::DSL.method_defined?(:run)` rather than `respond_to?(:postflight_steps)` — the latter is true on 6.0.0–6.0.12, where the block body would then die. That also corrects the floor the last commit documented: it is 6.0.13, not 7.0. Verified by regenerating the cask with GoReleaser built from source (both branches render, `{{staged_path}}` and `#{staged_path}` each survive the template pass, `ruby -c` clean) and by evaluating the emitted block against stub DSLs for all three generations: pre-6.0 and 6.0.0–6.0.12 pick `postflight`, 6.0.13+ picks `postflight_steps` and reaches `run` with the expected argv. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0122vGrcATX7433WuQ91iLm9 --- .goreleaser.yaml | 53 +++++++++++++++++++++++++++++++----------------- AGENTS.md | 34 ++++++++++++++++++++----------- README.md | 8 ++++---- 3 files changed, 60 insertions(+), 35 deletions(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 72718e3..535ef8a 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -205,36 +205,51 @@ homebrew_casks: # # This is a raw `custom_block` rather than GoReleaser's `hooks.post.install` # because that hook is emitted as Homebrew's `postflight do … end`, which - # Homebrew 7.0 deprecated in favour of the declarative `postflight_steps`: + # Homebrew 6.0.16 deprecated in favour of the declarative `postflight_steps`: # every `brew` command touching the cask printed # Warning: Calling `postflight` is deprecated! Use `postflight_steps` instead. # and Homebrew deprecations turn into errors a few releases later. GoReleaser # cannot emit the `*_steps` stanzas yet (goreleaser/goreleaser#6870, PR #6873, - # milestone v2.19.0) — when that lands, move this back to - # `hooks.post.install_steps` and delete the block. + # milestone v2.19.0) and has no way to emit both spellings, so the cask picks + # one at load time and stays installable on every Homebrew that ever shipped + # this cask. When GoReleaser grows `hooks.post.install_steps` it still cannot + # express the fallback, so this block outlives that. # - # Three things about the form below: - # - `custom_block` is injected right after `cask "koc" do`, so the stanza - # order in the generated file is unconventional. It is still correct: - # Homebrew orders artifacts by class (Binary, then PostflightSteps — see - # Cask::Artifact::AbstractArtifact.sort_order), never by file position, - # so this runs after the binary is staged and linked. + # Four things about the form below: + # - The probe is the *steps DSL*, not `respond_to?(:postflight_steps)`. The + # stanza landed in Homebrew 6.0.0 but the `run`/`on_macos` steps it needs + # only arrived in 6.0.13, so the obvious probe would pick the new branch + # on 6.0.0–6.0.12 and then die inside the block. # - `{{ "{{staged_path}}" }}` renders as the literal `{{staged_path}}`. # GoReleaser runs the whole generated cask through its template engine as # a final pass, so the Homebrew token has to survive it escaped; Homebrew - # expands it in step args at install time (the steps DSL has no Ruby - # interpolation — that was the point of deprecating `postflight`). + # expands it in step args at install time. The steps DSL has no Ruby + # interpolation — that was the point of deprecating `postflight` — which + # is why the two branches spell the same path differently. # - `run` is the steps DSL's escape hatch for a command; the sandbox it # runs under already grants write access to the caskroom, which is where # `staged_path` lives, so no `writable_paths` is needed. - # - # This requires Homebrew >= 7.0: an older client raises on the unknown - # stanza rather than warning. `brew install` auto-updates first, so in - # practice only HOMEBREW_NO_AUTO_UPDATE=1 users can hit that. + # - `custom_block` is injected right after `cask "koc" do`, so the stanza + # order in the generated file is unconventional. It is still correct: + # Homebrew orders artifacts by class (Binary, then PostflightSteps — see + # Cask::Artifact::AbstractArtifact.sort_order), never by file position, + # so this runs after the binary is staged and linked. custom_block: | - postflight_steps do - on_macos do - run "/usr/bin/xattr", - args: ["-dr", "com.apple.quarantine", "{{ "{{staged_path}}" }}/koc"] + # Homebrew >= 6.0.13 takes the declarative steps; older clients keep the + # `postflight` block, which is only deprecated from 6.0.16 on. + steps_dsl = defined?(Homebrew::InstallSteps::DSL) && Homebrew::InstallSteps::DSL + if steps_dsl && steps_dsl.method_defined?(:run) && steps_dsl.method_defined?(:on_macos) + postflight_steps do + on_macos do + run "/usr/bin/xattr", + args: ["-dr", "com.apple.quarantine", "{{ "{{staged_path}}" }}/koc"] + end + end + else + postflight do + if OS.mac? + system_command "/usr/bin/xattr", + args: ["-dr", "com.apple.quarantine", "#{staged_path}/koc"] + end end end diff --git a/AGENTS.md b/AGENTS.md index 325c596..f271dd5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -544,19 +544,29 @@ missing `completions/`. binary is unsigned, so the cask has to strip `com.apple.quarantine` from the staged `koc` or macOS refuses to launch it. GoReleaser emits `homebrew_casks.hooks.post.install` as Homebrew's `postflight do … end`, which -Homebrew 7.0 deprecated in favour of the declarative `postflight_steps` — every -`brew` command touching the cask printed a deprecation warning, and Homebrew -deprecations become errors a few releases later. GoReleaser cannot emit the -`*_steps` stanzas yet (goreleaser/goreleaser#6870, PR #6873, milestone v2.19.0), -so `.goreleaser.yaml` writes the stanza itself through `custom_block`. Three -things to keep in mind when touching it, all spelled out next to the block: the -Homebrew path token has to be escaped as `{{ "{{staged_path}}" }}` because +Homebrew 6.0.16 deprecated in favour of the declarative `postflight_steps` — +every `brew` command touching the cask printed a deprecation warning, and +Homebrew deprecations become errors a few releases later. GoReleaser cannot emit +the `*_steps` stanzas yet (goreleaser/goreleaser#6870, PR #6873, milestone +v2.19.0) and has no way to emit *both* spellings, so `.goreleaser.yaml` writes +the stanza itself through `custom_block`: the cask probes at load time and picks +`postflight_steps` on Homebrew >= 6.0.13, `postflight` below that, so it installs +on every Homebrew and warns on none. + +The three Homebrew versions that matter, because they are three releases apart +and the obvious probe gets it wrong: the `postflight_steps` **stanza** landed in +6.0.0, the `run`/`on_macos` **steps** the block actually calls landed in 6.0.13, +and `postflight` was **deprecated** in 6.0.16. So the probe asks +`Homebrew::InstallSteps::DSL.method_defined?(:run)`, not +`respond_to?(:postflight_steps)` — the latter is true on 6.0.0–6.0.12, where the +block body would then die. Two more things, both spelled out next to the block: +the Homebrew path token has to be escaped as `{{ "{{staged_path}}" }}` because GoReleaser runs the whole generated cask through its own template engine as a -final pass; `custom_block` lands right after `cask "koc" do`, which is fine -because Homebrew orders artifacts by class rather than by file position; and the -cask now requires Homebrew >= 7.0, since an older client raises on the unknown -stanza instead of warning. When GoReleaser ships `hooks.post.install_steps`, -move back to it and delete the block. +final pass (the legacy branch spells the same path `#{staged_path}`, since the +steps DSL has no Ruby interpolation); and `custom_block` lands right after +`cask "koc" do`, which is fine because Homebrew orders artifacts by class rather +than by file position. `hooks.post.install_steps`, when GoReleaser ships it, +still cannot express the fallback branch, so this block outlives it. **Builds are byte-reproducible, and it takes more than one setting.** `builds: mod_timestamp: {{ .CommitTimestamp }}` fixes the binary, but a tar/zip records diff --git a/README.md b/README.md index 23a50a0..d4369f8 100644 --- a/README.md +++ b/README.md @@ -54,10 +54,10 @@ brew install ftarasenko/tap/koc No `--cask` flag is needed — nothing else in the tap shares the name. The binary is unsigned, so the cask strips the macOS quarantine flag on install; on Apple -Silicon Go already ad-hoc-signs the binary so it runs. That step is written with -Homebrew's `postflight_steps` stanza, so the cask needs **Homebrew 7.0 or -newer** — `brew install` auto-updates itself before it reads the cask, so only -`HOMEBREW_NO_AUTO_UPDATE=1` users have to update by hand first. +Silicon Go already ad-hoc-signs the binary so it runs. That step picks its own +spelling at load time — Homebrew's declarative `postflight_steps` on 6.0.13 and +newer, the older `postflight` block below that — so the cask installs on any +Homebrew version and prints a deprecation warning on none. ### Shell completion