Skip to content

Sign + notarize macOS forkpress binaries in BK#385

Draft
mokagio wants to merge 2 commits into
mokagio/buildkite-ci-skeletonfrom
mokagio/buildkite-macos-codesign
Draft

Sign + notarize macOS forkpress binaries in BK#385
mokagio wants to merge 2 commits into
mokagio/buildkite-ci-skeletonfrom
mokagio/buildkite-macos-codesign

Conversation

@mokagio
Copy link
Copy Markdown

@mokagio mokagio commented May 19, 2026

Rationale

forkpress ships a Mac CLI binary outside the App Store. To pass Gatekeeper without quarantine warnings, the binary needs a Developer ID Application signature with hardened runtime, plus an Apple notarization ticket. This wires both into the BK mac queue build steps so every CI run on this pipeline produces a release-grade binary.

Distribution model is Developer ID + notarization — no App Store provisioning profile, no App Store Connect app record beyond the API key already in scope.

Intentional tradeoffs

  • entitlements.plist ships empty. The embedded static PHP runtime is built without opcache/JIT and links all deps statically, so no hardened-runtime opt-outs are needed today. Adding entitlements later is a one-line change.
  • Signing happens before the COW e2e on aarch64. The e2e exercises the hardened-runtime binary, so a regression introduced by signing (e.g. a future JIT toggle) breaks CI rather than shipping silently. Notarization runs after a green e2e to avoid wasting Apple's API quota on a binary we already know is broken.
  • No stapling. xcrun stapler only works on bundles/DMGs; for a raw CLI the notarization ticket is recorded against the binary's code-signature hash and resolved by Gatekeeper online lookup. Acceptable for a CLI distributed via tar.gz.
  • FORKPRESS_SKIP_NOTARIZE=1 escape hatch. Lets developers sign without paying the ~1–5 min notarytool round-trip, and gives a clean opt-out for any future branch run without App Store Connect creds in scope.

Gotchas

  • The signing identity has to be in the BK mac VM's codesigning keychain. If security find-identity -p codesigning doesn't surface a Developer ID Application: line for the team, the step exits with an actionable error. (Cert is expected to be present; if it isn't, that's a fleet-infrastructure follow-up rather than a script change.)
  • notarytool rejects the PEM body as invalidPEMDocument if the trailing newline is missing — the script writes the key with printf '%b\n' to handle that.

How to test

  • Push to this branch, watch the BK build. The aarch64 step logs ==> codesigning ... and ==> notarization accepted (id=...) ; the x86_64 step does the same back-to-back without an e2e in between.
  • Locally on a Mac with the cert in keychain: scripts/macos/codesign.sh target/aarch64-apple-darwin/release/forkpress then codesign --display --verbose=2 should report Authority=Developer ID Application: ... and the hardened-runtime flag.

Stacked on #312 — base is mokagio/buildkite-ci-skeleton, retarget to trunk after that merges.

@mokagio mokagio self-assigned this May 19, 2026
@mokagio mokagio force-pushed the mokagio/buildkite-macos-codesign branch 2 times, most recently from c34ecb7 to 9c0626a Compare May 19, 2026 07:28
@mokagio mokagio force-pushed the mokagio/buildkite-macos-codesign branch from 9c0626a to ee7027f Compare May 19, 2026 07:43
Adds Developer ID code signing and Apple notary submission for the
mac builds produced on the BK `mac` queue, distributed outside the
Mac App Store.

Cert installation reuses the a8c fastlane-match convention: a thin
`Fastfile` exposes a `set_up_signing` lane that pulls the Developer
ID cert from the shared S3 store into a temp CI keychain via
`sync_code_signing`. The lane is invoked from each mac BK step
before the codesign call; matches the platform-imessage and
workspace flows so the operational story is the same across a8c CLI
repos.

Three composable bash helpers under `scripts/macos/`:

- `codesign.sh` — Developer ID signing with hardened runtime and
  secure timestamp. Resolves the identity from the keychain by team
  id (default `PZYM8XX95Q`), with `FORKPRESS_CODESIGN_IDENTITY` /
  `--identity` overrides. Optional entitlements file.
- `notarize.sh` — submits a ditto-zipped Mach-O to `xcrun notarytool`
  with App Store Connect API key auth, waits for the verdict, prints
  the notary log on rejection.
- `sign-and-notarize.sh` — orchestrator. `--skip-notarize` (or
  `FORKPRESS_SKIP_NOTARIZE=1`) signs but skips the round-trip.

`entitlements.plist` ships with an empty `<dict/>`: forkpress's
embedded static PHP has opcache/JIT disabled and all deps are
statically linked, so no hardened-runtime opt-outs are required.

`mac-aarch64-build.sh` signs between `cargo build --release` and the
COW e2e so the e2e exercises the hardened-runtime binary the artifact
upload ships, then notarizes after a green e2e.

`mac-x86_64-build.sh` signs the cross-built x86_64 Mach-O as a
codesign smoke check but doesn't notarize: that binary uses
`FORKPRESS_RUNTIME_BUNDLE=/dev/null` so it has no embedded PHP and
isn't runnable, so submitting it to Apple's notary service would
just burn the API quota on a non-shippable artifact.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.7 <noreply@anthropic.com>
@mokagio mokagio force-pushed the mokagio/buildkite-macos-codesign branch 4 times, most recently from 5a4f2a2 to 37af782 Compare May 19, 2026 11:10
Drop the three bash helpers under `scripts/macos/` (`codesign.sh`,
`notarize.sh`, `sign-and-notarize.sh`) in favor of two new lanes in
`fastlane/Fastfile`:

- `sign_binary binary:<path>` — Developer ID codesign with hardened
  runtime + secure timestamp. Identity lookup uses the same keychain
  awk crawl as before; team id is now a Fastfile constant, no longer
  a CLI flag nobody overrode.
- `notarize_binary binary:<path>` — ditto-zips and submits to Apple
  via the built-in `notarize` action (`use_notarytool: true`,
  `skip_stapling: true` since a raw Mach-O can't be stapled). Auth
  picked up from `app_store_connect_api_key` reading the
  `APP_STORE_CONNECT_API_KEY_*` env vars. `bundle_id` is set so the
  action doesn't try to read it from a non-existent `Info.plist`,
  and `verbose: false` is required because the action otherwise
  interleaves notarytool debug lines into the JSON output it tries
  to parse.

Both lanes resolve `binary:` against `REPO_ROOT` because fastlane's
`sh` action runs commands from inside `fastlane/`, where a
`target/...` relative path doesn't exist — codesign then errors out
with "No such file or directory".

The entitlements path becomes a Fastfile constant pointing at
`scripts/macos/entitlements.plist`, which stays (it's the actual
plist that codesign needs).

BK steps now invoke `bundle exec fastlane sign_binary` / `notarize_
binary` directly. `bundle install` is already a sunk cost for the
`set_up_signing` lane, so consolidating onto fastlane removes the
parallel bash chain without adding new deps.

`fastlane/.gitignore` ignores the `README.md` + `report.xml` that
the fastlane CLI dumps after each run.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.7 <noreply@anthropic.com>
@mokagio mokagio force-pushed the mokagio/buildkite-macos-codesign branch from 37af782 to 2d47ae6 Compare May 19, 2026 11:34
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.

1 participant