Skip to content

ci: Sign and notarize the macOS standalone binaries - #609

Merged
razor-x merged 7 commits into
mainfrom
claude/macos-binary-signing-ci-1c2agd
Aug 5, 2026
Merged

ci: Sign and notarize the macOS standalone binaries#609
razor-x merged 7 commits into
mainfrom
claude/macos-binary-signing-ci-1c2agd

Conversation

@razor-x

@razor-x razor-x commented Aug 5, 2026

Copy link
Copy Markdown
Member

Summary

The released macOS binaries were cross-compiled on Linux and shipped unsigned, so Gatekeeper blocks them on any machine that downloads them. They are now built on a macOS runner, code signed with the Developer ID Application certificate under the hardened runtime, and notarized before release.

Build workflow

_build.yml splits into three jobs. Its artifact_name output and the final build-<sha> artifact are unchanged, so check.yml and publish.yml consume it exactly as before.

Job Runner Builds
linux ubuntu-latest npm package, Linux and Windows binaries, completions tarball
macos macos-latest darwin-x64 and darwin-arm64 binaries, signed and notarized
build ubuntu-latest merges both, generates checksums.txt

Checksums move to the last job because signing rewrites the macOS binaries.

Signing

  • apple-actions/import-codesign-certs imports the certificate into a temporary keychain and deletes it after the job.
  • codesign runs with --options runtime, a secure timestamp, and the three entitlements JavaScriptCore needs to start under the hardened runtime: allow-jit, allow-unsigned-executable-memory, and disable-library-validation.
  • One notarytool submit --wait covers both architectures, since the notary service notarizes every eligible binary in the archive. A ticket cannot be stapled to a bare executable, so Gatekeeper looks it up online by cdhash.
  • The job then verifies the signature, assesses the binary the way Gatekeeper does, runs it, and asserts it reports the package version.

Signing is required, never faked

A sign input controls it, rather than the presence of credentials:

  • Release and manual builds sign. Every credential is required, and the job fails in seconds, before it builds anything, naming whichever secret is unset.
  • Checks pass sign: false. A pull request from a fork cannot read the credentials, and nothing a check builds is released. Those binaries are reported as unreleasable.

There is no ad-hoc signature fallback: Gatekeeper rejects one anywhere but the machine that made it, so it would only produce binaries that look signed.

build.yml adds a manual entrypoint for running a full signing pass without cutting a release.

Two things worth knowing

Compiled binaries carry their JavaScript bundle in a __BUN segment with nothing trailing the Mach-O, so a signature lays out cleanly. Verified by parsing the segments of the binaries this repo produces.

Two details of the build would otherwise be silent bugs, and both are asserted in the job:

  • Bun 1.2.14 writes an LC_CODE_SIGNATURE on its darwin-x64 output whose datasize overruns the end of the file by 21,760 bytes, fixed in Bun 1.3. The signing step drops Bun's ad-hoc signature first so codesign lays out its own from scratch.
  • prepack injects the version into src/lib/version.ts, which the old single job got for free from npm pack. Without it the macOS binaries would report 0.0.0, so the macos job runs it and the verify step asserts the reported version.

Before this can release

  1. Set five repository secrets, documented under Signing the macOS binaries in the README: APPLE_CERTIFICATE, APPLE_CERTIFICATE_PASSWORD, APPLE_API_KEY, APPLE_API_KEY_ID, and APPLE_API_ISSUER_ID. Leave APPLE_SIGNING_IDENTITY unset unless the bundle holds more than one Developer ID Application certificate.
  2. Merge this. GitHub only offers a workflow_dispatch for a workflow already on the default branch, so Build cannot run until then.
  3. Run Build by hand to confirm the credentials work.

codesign and notarytool have not run yet, and nothing on this pull request exercises them: checks build with sign: false. That manual run is what proves the signing path end to end, before a tag depends on it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BoPNEdpFrKuJsfnDGcb33w

claude added 4 commits August 5, 2026 19:41
Build the darwin binaries on a macOS runner so they can be code signed with
the Developer ID Application certificate under the hardened runtime, then
notarized in a single notarytool submission covering both architectures.

The compiled binaries carry their JavaScript bundle in a __BUN segment with
no data trailing the Mach-O, so a signature lays out cleanly, and the job
proves it by verifying the signature and running the signed binary.

Signing is skipped, with a warning, when the Apple secrets are unset: the
binaries then only get an ad-hoc signature, which keeps pull request builds
working while exercising the same code path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BoPNEdpFrKuJsfnDGcb33w
Replace the hand-rolled keychain setup with apple-actions/import-codesign-certs,
which runs the same security commands and deletes the temporary keychain after
the job, leaving only the codesign and notarytool calls as scripts.

Drop the Apple ID and app-specific password notarization path: the notary
service is authenticated with an App Store Connect API key alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BoPNEdpFrKuJsfnDGcb33w
Move the manual trigger out of _build.yml and into a caller, following the
_publish.yml and publish.yml pair. A reusable workflow only ever populates
inputs from workflow_call, so serving a workflow_dispatch from the same file
meant defaulting every input again at each use.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BoPNEdpFrKuJsfnDGcb33w
Gatekeeper rejects an ad-hoc signature on any machine but the one that made
it, so falling back to one only produced binaries that look signed. Take a
sign input instead: when it is set, every Apple credential is required and the
job fails without them, and when it is not, the binaries are built but left
alone and reported as unreleasable.

Anything that releases signs. A check opts out, since a pull request from a
fork cannot read the credentials.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BoPNEdpFrKuJsfnDGcb33w
Comment thread .github/workflows/publish.yml Outdated
build:
name: Build
uses: ./.github/workflows/_build.yml
# Passes the Apple credentials used to sign and notarize the macOS binaries.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Suggested change
# Passes the Apple credentials used to sign and notarize the macOS binaries.

Comment thread .github/workflows/check.yml Outdated
Comment on lines +60 to +61
# A pull request from a fork cannot read the signing credentials,
# and a check never releases what it builds.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Suggested change
# A pull request from a fork cannot read the signing credentials,
# and a check never releases what it builds.

Comment thread .github/workflows/build.yml Outdated
build:
name: Build
uses: ./.github/workflows/_build.yml
# Checks the macOS signing credentials without cutting a release.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Suggested change
# Checks the macOS signing credentials without cutting a release.

Comment thread .github/workflows/_build.yml Outdated
name: build-${{ github.sha }}-macos
path: release
- name: Generate checksums
# Signing changes the macOS binaries, so checksum everything after it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Suggested change
# Signing changes the macOS binaries, so checksum everything after it.

Comment thread .github/workflows/_build.yml Outdated
if-no-files-found: error
path: release/*

bundle:

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is build

Comment thread .github/workflows/_build.yml Outdated

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

this is just linux (yes it also builds windows but we may need to sign those eventually to anyway)

linux builds the Linux and Windows binaries alongside the npm package, macos
builds and signs the macOS ones, and build assembles the artifact that both
feed. The job display names, and so the check names, stay as they were.

Drop the comments that only restate the line below them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BoPNEdpFrKuJsfnDGcb33w
@razor-x razor-x changed the title Add macOS code signing and notarization support ci: Sign and notarize the macOS standalone binaries Aug 5, 2026
claude added 2 commits August 5, 2026 21:44
The build resolves the one Developer ID Application identity in the imported
certificate, so the secret only matters when there is more than one to choose
between.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BoPNEdpFrKuJsfnDGcb33w
GitHub only offers a workflow_dispatch for a workflow on the default branch,
so the credentials cannot be checked this way until the workflow is merged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BoPNEdpFrKuJsfnDGcb33w
@razor-x
razor-x marked this pull request as ready for review August 5, 2026 21:52
@razor-x
razor-x merged commit 2c84d0f into main Aug 5, 2026
14 checks passed
@razor-x
razor-x deleted the claude/macos-binary-signing-ci-1c2agd branch August 5, 2026 21:52
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