ship a homebrew formula rather than a cask - #59
Merged
Merged
Conversation
certreader installed from the tap does not run:
$ certreader -version
$ echo $?
137
Nothing is printed, so it reads as a broken build rather than a refused
one. The bytes are fine: what homebrew installed is identical to the
release archive, and the same bytes run when extracted by hand, because
extracting by hand does not set com.apple.quarantine.
Homebrew marks what a cask installs with that attribute, and gatekeeper
kills a quarantined binary that carries no stapled notarization ticket.
A formula is not quarantined, which is how every other go command line
tool in homebrew arrives able to run. Formula binaries on this machine
carry no quarantine attribute at all; the cask's certreader does.
Signing and notarizing the binary was the first attempt at this and does
not settle it. There is nowhere to staple a ticket to in a bare
executable, stapler wanting an app bundle, a disk image or an installer
package, and on macos 26.5 a binary signed with a Developer ID
certificate and accepted by the notary service was still killed under
quarantine on three clean attempts across ten minutes, with spctl
calling it an unnotarized Developer ID and syspolicy_check saying the
ticket was missing. An earlier attempt appeared to succeed and was a
false pass: the kill puts a gatekeeper dialog on screen, and approving
one sets a bit in the attribute that lets that binary through
afterwards, which homebrew reads back as Quarantine::USER_APPROVED_FLAG.
The signing configuration stays, off unless a certificate is configured,
because it is worth having and is what a stapled package would be built
on. Preflight fails a half-configured set rather than publishing quietly
unsigned, since goreleaser skips signing when the certificate is absent.
goreleaser calls brews deprecated in favour of homebrew_casks and fails
its own check for a deprecation as well as for a mistake. Preflight now
tells those apart by goreleaser's wording rather than dropping the check.
Also documents brew trust, without which homebrew 6 refuses to load
anything from a third-party tap, and how to replace an installed cask.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WcPAJjNzG6bqy2FKqKKY1v
There was a problem hiding this comment.
🟡 Changes recommended
There are a few correctness/safety issues in the updated docs and workflow scripts (terminology mismatch, eval usage, secret scoping, and brittle message matching) that should be addressed before merging.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR changes the Homebrew distribution approach for certreader from a cask to a formula to avoid macOS Gatekeeper quarantine behavior that prevents the installed binary from running, and updates the release pipeline/docs accordingly.
Changes:
- Switch GoReleaser Homebrew output from
homebrew_caskstobrews, generatingFormula/certreader.rbin the tap. - Update the release workflow preflight to (a) allow GoReleaser “brews deprecation” checks while still failing real config errors, and (b) enforce “all-or-none” macOS signing secret configuration.
- Update README install/release documentation for formula-based installation and signing secret behavior.
File summaries
| File | Description |
|---|---|
| README.md | Updates brew install instructions and documents the formula-vs-cask rationale and secrets behavior. |
| .goreleaser/.goreleaser.darwin.yml | Adds optional macOS signing/notarization config and switches Homebrew publishing to a formula in Formula/. |
| .github/workflows/release.yml | Adds preflight checks for macOS signing secret completeness and tolerates GoReleaser deprecation-check failures by parsing output. |
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 5
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+88
to
+92
| if echo "$out" | grep -q "configuration is valid, but uses deprecated properties"; then | ||
| echo "::warning::$config uses deprecated goreleaser properties, tolerated because the replacement quarantines what it installs" | ||
| continue | ||
| fi | ||
| exit 1 |
Comment on lines
+180
to
+184
| MACOS_SIGN_P12: ${{ secrets.MACOS_SIGN_P12 }} | ||
| MACOS_SIGN_PASSWORD: ${{ secrets.MACOS_SIGN_PASSWORD }} | ||
| MACOS_NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }} | ||
| MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }} | ||
| MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }} |
| # rather have it than not. It is off unless the certificate is configured, and | ||
| # it is not what makes a homebrew install run: a bare binary has nowhere to | ||
| # staple a notarization ticket, and on macos 26 an unstapled ticket did not | ||
| # satisfy gatekeeper for a quarantined binary in testing. The formula above is |
Comment on lines
+632
to
+634
| The workflow needs a `RELEASE_TOKEN` repository secret: a personal access token with `repo` scope on | ||
| both `jonhadfield/certreader` and `jonhadfield/homebrew-certreader`. The token built into Actions | ||
| cannot write to another repository, and the darwin build pushes the cask update to the tap. Without | ||
| the secret the workflow stops at its preflight job and publishes nothing. | ||
| cannot write to another repository, and the darwin build pushes the cask update to the tap. |
Two of these were wrong rather than merely improvable. A comment placed above the notarize block said the formula "above" fixed the install when the brews block is below it, and the README still described the darwin build as pushing a cask update to the tap, which is the thing this branch stops doing. The preflight match no longer turns on one exact sentence from goreleaser. It asks whether the output says DEPRECATED and does not say the configuration is invalid, so a reworded deprecation still reads as one. Wording that changes past recognising fails the release rather than waving something through, and the check is exercised both ways: a deprecated config is tolerated, an invalid one is not. The signing credentials now reach only the job that signs. The linux and windows targets never read them, and they are built in a container that was never handed them, but there is no reason for them to be in that environment at all. Indirect expansion replaces eval for reading a variable by name, which is what bash is for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WcPAJjNzG6bqy2FKqKKY1v
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
certreaderinstalled from the tap does not run:Nothing is printed, so it reads as a broken build rather than a refused one. The bytes are fine — what Homebrew installed is byte-identical to the release archive, and the same bytes run when extracted by hand, because extracting by hand does not set
com.apple.quarantine.the cause
A cask marks what it installs with
com.apple.quarantine, and Gatekeeper kills a quarantined binary carrying no stapled notarization ticket. A bare executable has nowhere to keep one —staplerwants an app bundle, a disk image or an installer package. A formula is not quarantined, which is how every other Go CLI in Homebrew arrives able to run. On this machine every formula-installed binary (gh,jq,goreleaser) has no quarantine attribute; the cask'scertreaderdoes.why not just notarize it
That was the first attempt, and it does not settle it. On macOS 26.5 a binary signed with a Developer ID certificate and Accepted by the notary service was still killed under quarantine on three clean attempts across ten minutes —
spctlcalling itsource=Unnotarized Developer ID,syspolicy_checksayingNotary Ticket Missing.An earlier attempt appeared to succeed and was a false pass: the kill puts a Gatekeeper dialog on screen, and approving one sets a bit in the quarantine attribute that lets that binary through afterwards — the same bit Homebrew reads back as
Quarantine::USER_APPROVED_FLAG = 0x0040.The signing configuration stays, off unless a certificate is configured, because it is worth having and is what a stapled
.pkgwould be built on later.changes
homebrew_casks:→brews:,Casks/→Formula/brewsdeprecation by its own wording, and still fails on a real error (verified against a deliberately broken config)brew install certreader, how to replace an installed cask, andbrew trust, without which Homebrew 6 refuses a third-party tapThe cask has already been removed from
jonhadfield/homebrew-certreader; GoReleaser writesFormula/certreader.rbon this release.verification
The generated formula was built locally and read:
bin.install "certreader", per-archurl/sha256,depends_on :macos. Worth noting the Makefile'senv -u GITLAB_TOKEN -u GITEA_TOKENmatters — invoking GoReleaser directly produced GitLab URLs.🤖 Generated with Claude Code
https://claude.ai/code/session_01WcPAJjNzG6bqy2FKqKKY1v