build(release): macOS Developer ID signing + notarization (inert until secrets exist) - #2856
Draft
devarismeroxa wants to merge 1 commit into
Draft
build(release): macOS Developer ID signing + notarization (inert until secrets exist)#2856devarismeroxa wants to merge 1 commit into
devarismeroxa wants to merge 1 commit into
Conversation
…rop deprecated goreleaser keys Through v0.19.0 the darwin release binaries were ad-hoc signed only — `codesign -dv` reports `Signature=adhoc`, `spctl -a -vv` reports `rejected`. A browser download carries com.apple.quarantine, so Gatekeeper kills the binary with no output at all: exit 137, empty terminal, no dialog, nothing to search for. That is the README's first documented install route. It stayed invisible because neither CI nor curl sets the quarantine attribute; only a human using a browser hits it. Homebrew and install.sh are unaffected for the same reason. scripts/sign-darwin.sh runs as a GoReleaser post-build hook and signs + notarizes via quill, which works from the Linux release runner so no macOS runner is needed. quill is installed from a pinned release and checksum-verified rather than through anchore's curl|sh installer: this is the one job that produces the artifacts users execute. The hook is a no-op unless the target is darwin, the tag is not a nightly, and MACOS_SIGN_P12 is set. So this merges safely BEFORE the Apple credentials exist — the release behaves exactly as it does today and logs why it skipped. Nightlies never notarize: it costs an Apple round-trip of several minutes, the train runs daily, and nobody browser-downloads a nightly. Signing stays OFF until an Apple Developer Program membership provides a Developer ID Application certificate and an App Store Connect API key. docs/releases.md documents the five secrets and how to verify a signed release. Separately, .goreleaser.yml used two properties GoReleaser has deprecated (archives.builds, archives.format_overrides.format) while release.yml pins `version: latest`. When they are removed the release pipeline breaks at tag time — the worst possible moment. Migrated to archives.ids and formats. Verified by A/B snapshot build: 13 artifacts, byte-identical names before and after, and `goreleaser check` now reports no deprecations. Tier 3 (release tooling; no engine or data-path change). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016xE861dwb3MLgqEdWRECLY
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.
Draft on purpose. The signing path cannot be executed until an Apple Developer Program membership exists, so I will not claim it verified. Everything that can be verified without credentials has been, and is evidenced below. See "What I need from you".
Defect
Through v0.19.0 the darwin release binaries are ad-hoc signed only. A browser download carries
com.apple.quarantineand Gatekeeper kills the binary with no output at all:This is the README's first documented install route ("Download a pre-built binary ... and simply run it!"). Exit 137 and an empty terminal — no dialog, no reason, nothing to search for.
It stayed invisible because neither CI nor
curlsets the quarantine attribute; only a human with a browser hits it. Homebrew andinstall.share unaffected for the same reason. Found by an end-to-end DX audit.Approach
scripts/sign-darwin.shruns as a GoReleaser post-build hook and signs + notarizes with quill, which works from the Linux release runner — no macOS runner needed, no change to the job topology.It is inert by construction. The hook exits 0 without doing anything unless all of:
MACOS_SIGN_P12is set.So this is safe to merge before the credentials exist: the release behaves exactly as it does today and logs why it skipped. Nightlies never notarize — it costs an Apple round-trip of several minutes, the train runs daily, and nobody browser-downloads a nightly.
quill is installed from a pinned release with its checksum verified rather than anchore's
curl | shinstaller. This is the one job that produces the artifacts users execute; an unverified download here is a supply-chain hole. Checksume58c6f86...3fbfwas taken from quill's publishedchecksums.txtand the download+verify was executed locally.Bonus fix: a latent release-pipeline break
.goreleaser.ymlused two properties GoReleaser has deprecated (archives.builds,archives.format_overrides.format) whilerelease.ymlpins goreleaserversion: latest. When those are removed upstream, releases break at tag time — the worst possible moment to discover it. Pre-existing onmain; confirmed by A/B, not introduced here.Migrated to
archives.idsandformats.goreleaser checknow reports no deprecations (previously two).Verification — what I actually ran
goreleaser checkmainvs this branchdiffempty)dist/conduit_darwin_arm64_v8.0/conduit version→v0.20.0-nightly.20260905 darwin/arm64quill_0.7.1_linux_amd64.tar.gz: OK, extracts to a 27MB binarybash -n+shellcheckon the hooknodeis broken (syntax error: unexpected end of file). Checked MD013 (tables exempt per.markdownlint.yml), MD040, tabs and emphasis style by hand. CI is authoritative.Note on the goreleaser target string: it is
darwin_arm64_v8.0, notdarwin_arm64. The hook matchesdarwin_*, verified against real hook output.What I could NOT verify
The signing and notarization path itself. It requires an Apple Developer ID certificate that does not exist. No claim is made that signing works. The first real signed release is the test, and
docs/releases.mddocuments exactly how to verify it (download through a browser, thencodesign -dv/spctl -a/./conduit version).What I need from you
An Apple Developer Program membership (99 USD/year). There is no way to notarize without one — it is not a technical choice. Then five repo secrets, documented in
docs/releases.md:MACOS_SIGN_P12,MACOS_SIGN_P12_PASSWORD,MACOS_NOTARY_KEY,MACOS_NOTARY_KEY_ID,MACOS_NOTARY_ISSUERThe certificate type must be Developer ID Application specifically. "Apple Development" and "Apple Distribution" certs cannot notarize software distributed outside the App Store.
Interim mitigation
The README fix — stop telling macOS users to do the thing that silently dies, and document
xattr -d com.apple.quarantine— is landing separately in the stale-references PR, which is already editing that install section.Adversarial self-review
anchore/quill-action@v0. I checked and it 404s — it does not exist. Replaced with the pinned checksum-verified install. Flagging because it is exactly the kind of thing that looks plausible and would have failed only at tag time.v0.20.0-nightly.20260905), not a guess at the format..app/.dmg/.pkg). Apple publishes the ticket and Gatekeeper fetches it on first run, so a notarized CLI in a tarball is accepted with network access. Standard shape for a notarized CLI, documented indocs/releases.md, not a gap.skip=signin my snapshot runs skips goreleaser's ownsignsblock (unused here), not build hooks — confirmed by seeing the hook actually run.Risk tier
Tier 3 — release tooling. No engine, data-path or config-schema change. The one real risk is breaking the release job, which is why the artifact-name A/B and the inert-by-default design are the core of the verification.
🤖 Generated with Claude Code
https://claude.ai/code/session_016xE861dwb3MLgqEdWRECLY