Skip to content

ship a homebrew formula rather than a cask - #59

Merged
jonhadfield merged 2 commits into
mainfrom
feature/notarize-darwin-builds
Sep 2, 2026
Merged

jonhadfield merged 2 commits into
mainfrom
feature/notarize-darwin-builds

Conversation

@jonhadfield

Copy link
Copy Markdown
Owner

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 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 — stapler wants 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's certreader does.

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 — spctl calling it source=Unnotarized Developer ID, syspolicy_check saying Notary 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 .pkg would be built on later.

changes

  • homebrew_casks: → brews:, Casks/ → Formula/
  • preflight tolerates GoReleaser's brews deprecation by its own wording, and still fails on a real error (verified against a deliberately broken config)
  • signing secrets no longer block a release, but a half-configured set does fail — GoReleaser silently skips signing when the certificate is absent
  • README: brew install certreader, how to replace an installed cask, and brew trust, without which Homebrew 6 refuses a third-party tap

The cask has already been removed from jonhadfield/homebrew-certreader; GoReleaser writes Formula/certreader.rb on this release.

verification

The generated formula was built locally and read: bin.install "certreader", per-arch url/sha256, depends_on :macos. Worth noting the Makefile's env -u GITLAB_TOKEN -u GITEA_TOKEN matters — invoking GoReleaser directly produced GitLab URLs.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WcPAJjNzG6bqy2FKqKKY1v

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
Copilot AI lite review requested due to automatic review settings September 2, 2026 17:36

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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_casks to brews, generating Formula/certreader.rb in 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 thread .github/workflows/release.yml Outdated
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 thread .github/workflows/release.yml Outdated
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 }}
Comment thread .github/workflows/release.yml Outdated
Comment thread .goreleaser/.goreleaser.darwin.yml Outdated
# 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 thread README.md Outdated
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
@jonhadfield
jonhadfield merged commit 0c732c2 into main Sep 2, 2026
2 checks passed
@jonhadfield
jonhadfield deleted the feature/notarize-darwin-builds branch September 2, 2026 18:50
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.

2 participants