From 7700d10e8dbfdba481ea4a7cf578a993a2e06406 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Tue, 19 May 2026 20:20:51 +1000 Subject: [PATCH 1/4] Sign + notarize macOS forkpress binaries in BK MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 ``: 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 --- .buildkite/commands/mac-aarch64-build.sh | 17 + .buildkite/commands/mac-x86_64-build.sh | 12 + Gemfile | 6 + Gemfile.lock | 446 +++++++++++++++++++++++ fastlane/Fastfile | 48 +++ scripts/macos/codesign.sh | 87 +++++ scripts/macos/entitlements.plist | 6 + scripts/macos/notarize.sh | 94 +++++ scripts/macos/sign-and-notarize.sh | 85 +++++ 9 files changed, 801 insertions(+) create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 fastlane/Fastfile create mode 100755 scripts/macos/codesign.sh create mode 100644 scripts/macos/entitlements.plist create mode 100755 scripts/macos/notarize.sh create mode 100755 scripts/macos/sign-and-notarize.sh diff --git a/.buildkite/commands/mac-aarch64-build.sh b/.buildkite/commands/mac-aarch64-build.sh index c04c2962..0545d4b6 100755 --- a/.buildkite/commands/mac-aarch64-build.sh +++ b/.buildkite/commands/mac-aarch64-build.sh @@ -26,5 +26,22 @@ FORKPRESS_TARGET="$TARGET" scripts/build-dist.sh echo "--- :crab: cargo build --release forkpress ($TARGET)" cargo build --release --target "$TARGET" -p forkpress-cli --bin forkpress --locked +echo "--- :fastlane: Installing Developer ID cert via match" +bundle install +bundle exec fastlane set_up_signing + +# Sign before the e2e so the test exercises the same hardened-runtime +# binary the artifact upload ships. +echo "--- :lock: Codesigning forkpress ($TARGET)" +scripts/macos/codesign.sh "target/$TARGET/release/forkpress" \ + --entitlements scripts/macos/entitlements.plist + echo "--- :cow: COW strategy e2e (APFS sparsebundle)" FORKPRESS_FORCE_MACOS_APFS_SPARSEBUNDLE=1 tests/cow/e2e.sh "target/$TARGET/release/forkpress" + +if [ "${FORKPRESS_SKIP_NOTARIZE:-0}" = "1" ]; then + echo "--- :fast_forward: FORKPRESS_SKIP_NOTARIZE=1 — skipping notarization" +else + echo "--- :apple: Notarizing forkpress ($TARGET)" + scripts/macos/notarize.sh "target/$TARGET/release/forkpress" +fi diff --git a/.buildkite/commands/mac-x86_64-build.sh b/.buildkite/commands/mac-x86_64-build.sh index 15139bd8..5e8972bf 100755 --- a/.buildkite/commands/mac-x86_64-build.sh +++ b/.buildkite/commands/mac-x86_64-build.sh @@ -21,3 +21,15 @@ FORKPRESS_RUNTIME_BUNDLE=/dev/null cargo build --release --target "$TARGET" -p f ls -lh "target/$TARGET/release/forkpress" file "target/$TARGET/release/forkpress" || true + +echo "--- :fastlane: Installing Developer ID cert via match" +bundle install +bundle exec fastlane set_up_signing + +# Sign as a smoke check on the codesign chain, but skip notarization — +# this binary has no embedded runtime (built with FORKPRESS_RUNTIME_BUNDLE= +# /dev/null above) and is unrunnable, so notarizing it would waste the +# notary API quota on a non-shippable artifact. +echo "--- :lock: Codesigning forkpress ($TARGET)" +scripts/macos/codesign.sh "target/$TARGET/release/forkpress" \ + --entitlements scripts/macos/entitlements.plist diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..cfbda9db --- /dev/null +++ b/Gemfile @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +source 'https://rubygems.org' + +gem 'fastlane', '~> 2.233' +gem 'fastlane-plugin-wpmreleasetoolkit', '~> 14.4' diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 00000000..c180c922 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,446 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.8) + abbrev (0.1.2) + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) + artifactory (3.0.17) + atomos (0.1.3) + aws-eventstream (1.4.0) + aws-partitions (1.1250.0) + aws-sdk-core (3.247.0) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + base64 + bigdecimal + jmespath (~> 1, >= 1.6.1) + logger + aws-sdk-kms (1.125.0) + aws-sdk-core (~> 3, >= 3.247.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.222.0) + aws-sdk-core (~> 3, >= 3.247.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.12.1) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + base64 (0.3.0) + benchmark (0.5.0) + bigdecimal (4.1.2) + buildkit (1.6.1) + sawyer (>= 0.6) + chroma (0.2.0) + claide (1.1.0) + colored (1.2) + colored2 (3.1.2) + commander (4.6.0) + highline (~> 2.0.0) + csv (3.3.5) + declarative (0.0.20) + diffy (3.4.4) + digest-crc (0.7.0) + rake (>= 12.0.0, < 14.0.0) + domain_name (0.6.20240107) + dotenv (2.8.1) + emoji_regex (3.2.3) + erubi (1.13.1) + excon (0.112.0) + faraday (1.10.5) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.0) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + faraday-retry (~> 1.0) + ruby2_keywords (>= 0.0.4) + faraday-cookie_jar (0.0.8) + faraday (>= 0.8.0) + http-cookie (>= 1.0.0) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.1) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-multipart (1.2.0) + multipart-post (~> 2.0) + faraday-net_http (1.0.2) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday-retry (1.0.4) + faraday_middleware (1.2.1) + faraday (~> 1.0) + fastimage (2.4.1) + fastlane (2.234.0) + CFPropertyList (>= 2.3, < 5.0.0) + abbrev (~> 0.1) + addressable (>= 2.8, < 3.0.0) + artifactory (~> 3.0) + aws-sdk-s3 (~> 1.197) + babosa (>= 1.0.3, < 2.0.0) + base64 (~> 0.2) + benchmark (>= 0.1.0) + bundler (>= 1.17.3, < 5.0.0) + colored (~> 1.2) + commander (~> 4.6) + csv (~> 3.3) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 1.0.0) + faraday (~> 1.0) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (~> 1.0) + fastimage (>= 2.1.0, < 3.0.0) + fastlane-sirp (>= 1.1.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.3) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, <= 2.1.1) + google-cloud-storage (~> 1.31) + highline (~> 2.0) + http-cookie (~> 1.0.5) + json (< 3.0.0) + jwt (>= 2.1.0, < 3) + logger (>= 1.6, < 2.0) + mini_magick (>= 4.9.4, < 5.0.0) + multipart-post (>= 2.0.0, < 3.0.0) + mutex_m (~> 0.3) + naturally (~> 2.2) + nkf (~> 0.2) + optparse (>= 0.1.1, < 1.0.0) + ostruct (>= 0.1.0) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.5) + simctl (~> 1.6.3) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (~> 3) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.4.1) + xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) + fastlane-plugin-wpmreleasetoolkit (14.6.0) + buildkit (~> 1.5) + chroma (= 0.2.0) + diffy (~> 3.3) + dotenv (~> 2.8) + fastlane (~> 2.231) + gettext (~> 3.5) + git (~> 1.3) + google-cloud-storage (~> 1.31) + java-properties (~> 0.3.0) + nokogiri (~> 1.19, >= 1.19.3) + octokit (~> 6.1) + parallel (~> 1.14) + plist (~> 3.1) + progress_bar (~> 1.3) + rake (>= 12.3, < 14.0) + rake-compiler (~> 1.0) + xcodeproj (~> 1.22) + fastlane-sirp (1.1.0) + fiddle (1.1.8) + forwardable (1.4.0) + gettext (3.5.2) + erubi + locale (>= 2.0.5) + prime + racc + text (>= 1.3.0) + gh_inspector (1.1.3) + git (1.19.1) + addressable (~> 2.8) + rchardet (~> 1.8) + google-apis-androidpublisher_v3 (0.100.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-core (0.18.0) + addressable (~> 2.5, >= 2.5.1) + googleauth (~> 1.9) + httpclient (>= 2.8.3, < 3.a) + mini_mime (~> 1.0) + mutex_m + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + google-apis-iamcredentials_v1 (0.27.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-playcustomapp_v1 (0.17.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-storage_v1 (0.62.0) + google-apis-core (>= 0.15.0, < 2.a) + google-cloud-core (1.8.0) + google-cloud-env (>= 1.0, < 3.a) + google-cloud-errors (~> 1.0) + google-cloud-env (2.1.1) + faraday (>= 1.0, < 3.a) + google-cloud-errors (1.6.0) + google-cloud-storage (1.60.0) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-core (>= 0.18, < 2) + google-apis-iamcredentials_v1 (~> 0.18) + google-apis-storage_v1 (>= 0.42) + google-cloud-core (~> 1.6) + googleauth (~> 1.9) + mini_mime (~> 1.0) + googleauth (1.11.2) + faraday (>= 1.0, < 3.a) + google-cloud-env (~> 2.1) + jwt (>= 1.4, < 3.0) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (>= 0.16, < 2.a) + highline (2.0.3) + http-cookie (1.0.8) + domain_name (~> 0.5) + httpclient (2.9.0) + mutex_m + java-properties (0.3.0) + jmespath (1.6.2) + json (2.19.5) + jwt (2.10.2) + base64 + locale (2.1.5) + fiddle + logger (1.7.0) + mini_magick (4.13.2) + mini_mime (1.1.5) + multi_json (1.21.1) + multipart-post (2.4.1) + mutex_m (0.3.0) + nanaimo (0.4.0) + naturally (2.3.0) + nkf (0.2.0) + nokogiri (1.19.3-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.3-aarch64-linux-musl) + racc (~> 1.4) + nokogiri (1.19.3-arm-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.3-arm-linux-musl) + racc (~> 1.4) + nokogiri (1.19.3-arm64-darwin) + racc (~> 1.4) + nokogiri (1.19.3-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.19.3-x86_64-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.3-x86_64-linux-musl) + racc (~> 1.4) + octokit (6.1.1) + faraday (>= 1, < 3) + sawyer (~> 0.9) + options (2.3.2) + optparse (0.8.1) + os (1.1.4) + ostruct (0.6.3) + parallel (1.28.0) + plist (3.7.2) + prime (0.1.4) + forwardable + singleton + progress_bar (1.3.4) + highline (>= 1.6) + options (~> 2.3.0) + public_suffix (7.0.5) + racc (1.8.1) + rake (13.4.2) + rake-compiler (1.3.1) + rake + rchardet (1.10.0) + representable (3.2.0) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (3.4.1) + rexml (3.4.4) + rouge (3.28.0) + ruby2_keywords (0.0.5) + rubyzip (2.4.1) + sawyer (0.9.3) + addressable (>= 2.3.5) + faraday (>= 0.17.3, < 3) + security (0.1.5) + signet (0.21.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 4.0) + multi_json (~> 1.10) + simctl (1.6.10) + CFPropertyList + naturally + singleton (0.3.0) + terminal-notifier (2.0.0) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + text (1.3.1) + trailblazer-option (0.1.2) + tty-cursor (0.7.1) + tty-screen (0.8.2) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + uber (0.1.0) + unicode-display_width (2.6.0) + word_wrap (1.0.0) + xcodeproj (1.27.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.4.0) + rexml (>= 3.3.6, < 4.0) + xcpretty (0.4.1) + rouge (~> 3.28.0) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + aarch64-linux-gnu + aarch64-linux-musl + arm-linux-gnu + arm-linux-musl + arm64-darwin + x86_64-darwin + x86_64-linux-gnu + x86_64-linux-musl + +DEPENDENCIES + fastlane (~> 2.233) + fastlane-plugin-wpmreleasetoolkit (~> 14.4) + +CHECKSUMS + CFPropertyList (3.0.8) sha256=2c99d0d980536d3d7ab252f7bd59ac8be50fbdd1ff487c98c949bb66bb114261 + abbrev (0.1.2) sha256=ad1b4eaaaed4cb722d5684d63949e4bde1d34f2a95e20db93aecfe7cbac74242 + addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af + artifactory (3.0.17) sha256=3023d5c964c31674090d655a516f38ca75665c15084140c08b7f2841131af263 + atomos (0.1.3) sha256=7d43b22f2454a36bace5532d30785b06de3711399cb1c6bf932573eda536789f + aws-eventstream (1.4.0) sha256=116bf85c436200d1060811e6f5d2d40c88f65448f2125bc77ffce5121e6e183b + aws-partitions (1.1250.0) sha256=150a1cf3edfd260ff9f84707f4ee72b2977bbc4b43472431e0f3f6bdc3971537 + aws-sdk-core (3.247.0) sha256=789864594ce8cef05ee3d81fa8ed506099280bda6ea12a7612b8b7c5e5e62851 + aws-sdk-kms (1.125.0) sha256=23f81bc0838ae6ec2e8de3eae88af521d0e29d3a59b6c9dbb4b21343ba476bc8 + aws-sdk-s3 (1.222.0) sha256=bae4b06fccf0b81b8d77e7abfc56e5e2146590d43c4ab58db0cee3ff5bbfb7f1 + aws-sigv4 (1.12.1) sha256=6973ff95cb0fd0dc58ba26e90e9510a2219525d07620c8babeb70ef831826c00 + babosa (1.0.4) sha256=18dea450f595462ed7cb80595abd76b2e535db8c91b350f6c4b3d73986c5bc99 + base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b + benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c + bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd + buildkit (1.6.1) sha256=e0e51c0ce3334654c356bdef3410cba5c9b36b336d0d6542f3ecfb477c5fa97c + bundler (4.0.11) sha256=5bcec0fb78302e48d02ee46f10ee6e6942be647ba5b44a6d1ddfda9a240ce785 + chroma (0.2.0) sha256=64bdcd36a4765fbcd45adc64960cc153101300b4918f90ffdd89f4e2eb954b54 + claide (1.1.0) sha256=6d3c5c089dde904d96aa30e73306d0d4bd444b1accb9b3125ce14a3c0183f82e + colored (1.2) sha256=9d82b47ac589ce7f6cab64b1f194a2009e9fd00c326a5357321f44afab2c1d2c + colored2 (3.1.2) sha256=b13c2bd7eeae2cf7356a62501d398e72fde78780bd26aec6a979578293c28b4a + commander (4.6.0) sha256=7d1ddc3fccae60cc906b4131b916107e2ef0108858f485fdda30610c0f2913d9 + csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f + declarative (0.0.20) sha256=8021dd6cb17ab2b61233c56903d3f5a259c5cf43c80ff332d447d395b17d9ff9 + diffy (3.4.4) sha256=79384ab5ca82d0e115b2771f0961e27c164c456074bd2ec46b637ebf7b6e47e3 + digest-crc (0.7.0) sha256=64adc23a26a241044cbe6732477ca1b3c281d79e2240bcff275a37a5a0d78c07 + domain_name (0.6.20240107) sha256=5f693b2215708476517479bf2b3802e49068ad82167bcd2286f899536a17d933 + dotenv (2.8.1) sha256=c5944793349ae03c432e1780a2ca929d60b88c7d14d52d630db0508c3a8a17d8 + emoji_regex (3.2.3) sha256=ecd8be856b7691406c6bf3bb3a5e55d6ed683ffab98b4aa531bb90e1ddcc564b + erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9 + excon (0.112.0) sha256=daf9ac3a4c2fc9aa48383a33da77ecb44fa395111e973084d5c52f6f214ae0f0 + faraday (1.10.5) sha256=b144f1d2b045652fa820b5f532723e1643cc28b93dae911d784e5c5f88e8f6ed + faraday-cookie_jar (0.0.8) sha256=0140605823f8cc63c7028fccee486aaed8e54835c360cffc1f7c8c07c4299dbb + faraday-em_http (1.0.0) sha256=7a3d4c7079789121054f57e08cd4ef7e40ad1549b63101f38c7093a9d6c59689 + faraday-em_synchrony (1.0.1) sha256=bf3ce45dcf543088d319ab051f80985ea6d294930635b7a0b966563179f81750 + faraday-excon (1.1.0) sha256=b055c842376734d7f74350fe8611542ae2000c5387348d9ba9708109d6e40940 + faraday-httpclient (1.0.1) sha256=4c8ff1f0973ff835be8d043ef16aaf54f47f25b7578f6d916deee8399a04d33b + faraday-multipart (1.2.0) sha256=7d89a949693714176f612323ca13746a2ded204031a6ba528adee788694ef757 + faraday-net_http (1.0.2) sha256=63992efea42c925a20818cf3c0830947948541fdcf345842755510d266e4c682 + faraday-net_http_persistent (1.2.0) sha256=0b0cbc8f03dab943c3e1cc58d8b7beb142d9df068b39c718cd83e39260348335 + faraday-patron (1.0.0) sha256=dc2cd7b340bb3cc8e36bcb9e6e7eff43d134b6d526d5f3429c7a7680ddd38fa7 + faraday-rack (1.0.0) sha256=ef60ec969a2bb95b8dbf24400155aee64a00fc8ba6c6a4d3968562bcc92328c0 + faraday-retry (1.0.4) sha256=dc659233777fabf96c69c2ffe56c0a5d2c102af90321a42cc6c90157bcd716aa + faraday_middleware (1.2.1) sha256=d45b78c8ee864c4783fbc276f845243d4a7918a67301c052647bacabec0529e9 + fastimage (2.4.1) sha256=c64bebd46b6fd8943ab70c1e6e85ff728f970f2e48f92ecd249b6bc3a540ad20 + fastlane (2.234.0) sha256=b74835681ad9a8e9c0931a5727dad1bab433895ac534c864a1ed5749625d26e9 + fastlane-plugin-wpmreleasetoolkit (14.6.0) sha256=33d32d117ae1a71c46b89c797c49c6722f5d2ef088330f89e996c879f1803f46 + fastlane-sirp (1.1.0) sha256=10bc94f9682efd8e1badfb31452a76dd8981f1f3a33717c765fde6d75b54d847 + fiddle (1.1.8) sha256=7fa8ee3627271497f3add5503acdbc3f40b32f610fc1cf49634f083ef3f32eee + forwardable (1.4.0) sha256=f1cd40cc9812937980e1c76f1aa053660990a7c9b6a98fc37d945468afcce838 + gettext (3.5.2) sha256=ada02c59aa7e9f56bd2522faedaed16421dd2f3ddb5fe28628c0be5abcbf3c74 + gh_inspector (1.1.3) sha256=04cca7171b87164e053aa43147971d3b7f500fcb58177698886b48a9fc4a1939 + git (1.19.1) sha256=b0a422d9f6517353c48a330d6114de4db9e0c82dbe7202964a1d9f1fbc827d70 + google-apis-androidpublisher_v3 (0.100.0) sha256=7a82935bee985190e8fe23bf5e53df3a27d65dd084114bb71b846b617de16489 + google-apis-core (0.18.0) sha256=96b057816feeeab448139ed5b5c78eab7fc2a9d8958f0fbc8217dedffad054ee + google-apis-iamcredentials_v1 (0.27.0) sha256=9289f29968610754ef11d98b9ec627f0153f3e2616fef839aef096de529f6d1e + google-apis-playcustomapp_v1 (0.17.0) sha256=d5bc90b705f3f862bab4998086449b0abe704ee1685a84821daa90ca7fa95a78 + google-apis-storage_v1 (0.62.0) sha256=f62467c36df53287fb0252ebb4da85f9e25d7b4c5809d045c2aab1fc307760c1 + google-cloud-core (1.8.0) sha256=e572edcbf189cfcab16590628a516cec3f4f63454b730e59f0b36575120281cf + google-cloud-env (2.1.1) sha256=cf4bb8c7d517ee1ea692baedf06e0b56ce68007549d8d5a66481aa9f97f46999 + google-cloud-errors (1.6.0) sha256=1da8476dd706ad04b9d32e3c4b90d07d3463b37d6407cb56d41342ea7647d0a1 + google-cloud-storage (1.60.0) sha256=b21b752d37945d678a4533be5ef4303f15d33a964d8bc709c7c41c3600f650db + googleauth (1.11.2) sha256=7e6bacaeed7aea3dd66dcea985266839816af6633e9f5983c3c2e0e40a44731e + highline (2.0.3) sha256=2ddd5c127d4692721486f91737307236fe005352d12a4202e26c48614f719479 + http-cookie (1.0.8) sha256=b14fe0445cf24bf9ae098633e9b8d42e4c07c3c1f700672b09fbfe32ffd41aa6 + httpclient (2.9.0) sha256=4b645958e494b2f86c2f8a2f304c959baa273a310e77a2931ddb986d83e498c8 + java-properties (0.3.0) sha256=0a9fdda90c25ba9ba4de0e242d954a5688629652b592aab66ed54e2b16b93093 + jmespath (1.6.2) sha256=238d774a58723d6c090494c8879b5e9918c19485f7e840f2c1c7532cf84ebcb1 + json (2.19.5) sha256=218a18553e4801d579ca7e0f5bc72bafd776d7397238a1fb4e74db5b0a812c59 + jwt (2.10.2) sha256=31e1ee46f7359883d5e622446969fe9c118c3da87a0b1dca765ce269c3a0c4f4 + locale (2.1.5) sha256=1c6803e8aa6bdb2c29e91945d095050601bf6d58474993575adf6f3b89b32ef4 + logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 + mini_magick (4.13.2) sha256=71d6258e0e8a3d04a9a0a09784d5d857b403a198a51dd4f882510435eb95ddd9 + mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef + multi_json (1.21.1) sha256=e6126a31808e3b4d19f483c775ceac34df190dffa62adfb63a165ee14ba68080 + multipart-post (2.4.1) sha256=9872d03a8e552020ca096adadbf5e3cb1cd1cdd6acd3c161136b8a5737cdb4a8 + mutex_m (0.3.0) sha256=cfcb04ac16b69c4813777022fdceda24e9f798e48092a2b817eb4c0a782b0751 + nanaimo (0.4.0) sha256=faf069551bab17f15169c1f74a1c73c220657e71b6e900919897a10d991d0723 + naturally (2.3.0) sha256=459923cf76c2e6613048301742363200c3c7e4904c324097d54a67401e179e01 + nkf (0.2.0) sha256=fbc151bda025451f627fafdfcb3f4f13d0b22ae11f58c6d3a2939c76c5f5f126 + nokogiri (1.19.3-aarch64-linux-gnu) sha256=46b89e5d7b9e844c2ee360794240c6ea2a4e6fa0c5892a4ed487db621224b639 + nokogiri (1.19.3-aarch64-linux-musl) sha256=8392dfdcd21be7a94dbbe9ccc138dea01b97b24cb2dc02a114ca98bfb1d9a0b7 + nokogiri (1.19.3-arm-linux-gnu) sha256=3919d5ffc334ad778a4a9eb88fda7dcb8b1fb58c8a52ac640c6dcd2f038e774f + nokogiri (1.19.3-arm-linux-musl) sha256=9ce1cb6346bb9c67b1550eb537aa183ead91e4b6eadb2f36ade02d8dd2a79fb6 + nokogiri (1.19.3-arm64-darwin) sha256=71b9bd424b1b7abc18b05052a1a3cfd3627abdca62be280854cc411791357e42 + nokogiri (1.19.3-x86_64-darwin) sha256=77f3fba57d46c53ab31e62fc6c28f705109d1bf6264356c76f132b2be5728d4d + nokogiri (1.19.3-x86_64-linux-gnu) sha256=2f5078620fe12e83669b5b17311b32532a8153d02eee7ad06948b926d6080976 + nokogiri (1.19.3-x86_64-linux-musl) sha256=248c906d2166eca5efb56d52fdee5f9a1f51d69a72e2b64fdac647b4ce39ea3f + octokit (6.1.1) sha256=920e4a9d820205f70738f58de6a7e6ef0e2f25b27db954b5806a63105207b0bf + options (2.3.2) sha256=32413a4b9e363234eed2eecfb2a1a9deb32810f72c54820a37a62f65b905c5e8 + optparse (0.8.1) sha256=42bea10d53907ccff4f080a69991441d611fbf8733b60ed1ce9ee365ce03bd1a + os (1.1.4) sha256=57816d6a334e7bd6aed048f4b0308226c5fb027433b67d90a9ab435f35108d3f + ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912 + parallel (1.28.0) sha256=33e6de1484baf2524792d178b0913fc8eb94c628d6cfe45599ad4458c638c970 + plist (3.7.2) sha256=d37a4527cc1116064393df4b40e1dbbc94c65fa9ca2eec52edf9a13616718a42 + prime (0.1.4) sha256=4d755ebf7c2994a6f3a3fee0d072063be3fff2d4042ebff6cd5eebd4747a225e + progress_bar (1.3.4) sha256=adb10e040275e08eadfbe405749584e4b01fd15e8e692fdcb4b1969e9c071c8c + public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623 + racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f + rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701 + rake-compiler (1.3.1) sha256=6b351612b6e2d73ddd5563ee799bb58685176e05363db6758504bd11573d670a + rchardet (1.10.0) sha256=d5ea2ed61a720a220f1914778208e718a0c7ed2a484b6d357ba695aa7001390f + representable (3.2.0) sha256=cc29bf7eebc31653586849371a43ffe36c60b54b0a6365b5f7d95ec34d1ebace + retriable (3.4.1) sha256=fb3f114b7d492121c158c01f3d5152b5a615c5b70d5877d0bc08c7ec3725c3bc + rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142 + rouge (3.28.0) sha256=0d6de482c7624000d92697772ab14e48dca35629f8ddf3f4b21c99183fd70e20 + ruby2_keywords (0.0.5) sha256=ffd13740c573b7301cf7a2e61fc857b2a8e3d3aff32545d6f8300d8bae10e3ef + rubyzip (2.4.1) sha256=8577c88edc1fde8935eb91064c5cb1aef9ad5494b940cf19c775ee833e075615 + sawyer (0.9.3) sha256=0d0f19298408047037638639fe62f4794483fb04320269169bd41af2bdcf5e41 + security (0.1.5) sha256=3a977a0eca7706e804c96db0dd9619e0a94969fe3aac9680fcfc2bf9b8a833b7 + signet (0.21.0) sha256=d617e9fbf24928280d39dcfefba9a0372d1c38187ffffd0a9283957a10a8cd5b + simctl (1.6.10) sha256=b99077f4d13ad81eace9f86bf5ba4df1b0b893a4d1b368bd3ed59b5b27f9236b + singleton (0.3.0) sha256=83ea1bca5f4aa34d00305ab842a7862ea5a8a11c73d362cb52379d94e9615778 + terminal-notifier (2.0.0) sha256=7a0d2b2212ab9835c07f4b2e22a94cff64149dba1eed203c04835f7991078cea + terminal-table (3.0.2) sha256=f951b6af5f3e00203fb290a669e0a85c5dd5b051b3b023392ccfd67ba5abae91 + text (1.3.1) sha256=2fbbbc82c1ce79c4195b13018a87cbb00d762bda39241bb3cdc32792759dd3f4 + trailblazer-option (0.1.2) sha256=20e4f12ea4e1f718c8007e7944ca21a329eee4eed9e0fa5dde6e8ad8ac4344a3 + tty-cursor (0.7.1) sha256=79534185e6a777888d88628b14b6a1fdf5154a603f285f80b1753e1908e0bf48 + tty-screen (0.8.2) sha256=c090652115beae764336c28802d633f204fb84da93c6a968aa5d8e319e819b50 + tty-spinner (0.9.3) sha256=0e036f047b4ffb61f2aa45f5a770ec00b4d04130531558a94bfc5b192b570542 + uber (0.1.0) sha256=5beeb407ff807b5db994f82fa9ee07cfceaa561dad8af20be880bc67eba935dc + unicode-display_width (2.6.0) sha256=12279874bba6d5e4d2728cef814b19197dbb10d7a7837a869bab65da943b7f5a + word_wrap (1.0.0) sha256=f556d4224c812e371000f12a6ee8102e0daa724a314c3f246afaad76d82accc7 + xcodeproj (1.27.0) sha256=8cc7a73b4505c227deab044dce118ede787041c702bc47636856a2e566f854d3 + xcpretty (0.4.1) sha256=b14c50e721f6589ee3d6f5353e2c2cfcd8541fa1ea16d6c602807dd7327f3892 + xcpretty-travis-formatter (1.0.1) sha256=aacc332f17cb7b2cba222994e2adc74223db88724fe76341483ad3098e232f93 + +BUNDLED WITH + 4.0.11 diff --git a/fastlane/Fastfile b/fastlane/Fastfile new file mode 100644 index 00000000..2aa048fa --- /dev/null +++ b/fastlane/Fastfile @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +UI.user_error!('Please run fastlane via `bundle exec`') unless FastlaneCore::Helper.bundler? + +APPLE_TEAM_ID = 'PZYM8XX95Q' + +CODE_SIGNING_STORAGE_OPTIONS = { + storage_mode: 's3', + s3_bucket: 'a8c-fastlane-match', + s3_region: 'us-east-2' +}.freeze + +CODE_SIGNING_ENV_VARS = %w[ + MATCH_S3_ACCESS_KEY + MATCH_S3_SECRET_ACCESS_KEY + MATCH_PASSWORD +].freeze + +ASC_API_KEY_ENV_VARS = %w[ + APP_STORE_CONNECT_API_KEY_KEY_ID + APP_STORE_CONNECT_API_KEY_ISSUER_ID + APP_STORE_CONNECT_API_KEY_KEY +].freeze + +require 'fastlane/plugin/wpmreleasetoolkit' + +EnvManager = Fastlane::Wpmreleasetoolkit::EnvManager + +before_all do + setup_ci + EnvManager.set_up(env_file_name: 'forkpress.env') +end + +desc 'Fetch the Developer ID Application certificate into the keychain' +lane :set_up_signing do |readonly: true| + EnvManager.require_env_vars!(*CODE_SIGNING_ENV_VARS) + EnvManager.require_env_vars!(*ASC_API_KEY_ENV_VARS) unless readonly + + sync_code_signing( + type: 'developer_id', + platform: 'macos', + team_id: APPLE_TEAM_ID, + app_identifier: [], + api_key: readonly ? nil : app_store_connect_api_key, + readonly: readonly, + **CODE_SIGNING_STORAGE_OPTIONS + ) +end diff --git a/scripts/macos/codesign.sh b/scripts/macos/codesign.sh new file mode 100755 index 00000000..774ebdfc --- /dev/null +++ b/scripts/macos/codesign.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Codesign a macOS binary with a Developer ID Application identity + +# hardened runtime + secure timestamp. +# +# Usage: +# scripts/macos/codesign.sh +# [--team-id TEAM] +# [--entitlements FILE] +# [--identity 'Developer ID Application: ...'] +# +# Identity is resolved from the codesigning keychain by team id. +# FORKPRESS_CODESIGN_IDENTITY overrides the lookup. + +team_id="PZYM8XX95Q" +entitlements="" +identity="${FORKPRESS_CODESIGN_IDENTITY:-}" +binary="" + +usage() { + printf "usage: %s [--team-id TEAM] [--entitlements FILE] [--identity IDENTITY]\n" "${0##*/}" +} + +while [ $# -gt 0 ]; do + case "$1" in + --team-id) + [ $# -ge 2 ] || { printf >&2 "missing value for --team-id\n"; exit 1; } + team_id="$2"; shift 2 ;; + --entitlements) + [ $# -ge 2 ] || { printf >&2 "missing value for --entitlements\n"; exit 1; } + entitlements="$2"; shift 2 ;; + --identity) + [ $# -ge 2 ] || { printf >&2 "missing value for --identity\n"; exit 1; } + identity="$2"; shift 2 ;; + -h|--help) + usage; exit 0 ;; + -*) + printf >&2 "unknown arg: %s\n" "$1"; usage >&2; exit 1 ;; + *) + if [ -n "$binary" ]; then + printf >&2 "extra positional argument: %s\n" "$1"; usage >&2; exit 1 + fi + binary="$1"; shift ;; + esac +done + +if [ -z "$binary" ]; then + usage >&2; exit 1 +fi +if [ ! -f "$binary" ]; then + printf >&2 "binary not found: %s\n" "$binary"; exit 1 +fi + +if [ -z "$identity" ]; then + identity="$(security find-identity -v -p codesigning | awk -v team="(${team_id})" ' + /Developer ID Application:/ && index($0, team) { + sub(/^[^"]*"/, "") + sub(/"[^"]*$/, "") + print + exit + } + ')" +fi +if [ -z "$identity" ]; then + printf >&2 "no Developer ID Application identity for team %s in keychain\n" "$team_id" + printf >&2 "(set FORKPRESS_CODESIGN_IDENTITY=... or pass --identity to override)\n" + exit 1 +fi + +printf "==> codesigning %s\n identity: %s\n" "$binary" "$identity" + +codesign_args=(--force --options runtime --timestamp --sign "$identity") +if [ -n "$entitlements" ]; then + if [ ! -f "$entitlements" ]; then + printf >&2 "entitlements file not found: %s\n" "$entitlements"; exit 1 + fi + codesign_args+=(--entitlements "$entitlements") +fi + +codesign "${codesign_args[@]}" "$binary" + +printf "==> verifying signature\n" +codesign --verify --strict --verbose=2 "$binary" +codesign --display --verbose=2 "$binary" 2>&1 \ + | grep -E 'Authority|TeamIdentifier|Signature|flags|Identifier' || true diff --git a/scripts/macos/entitlements.plist b/scripts/macos/entitlements.plist new file mode 100644 index 00000000..6631ffa6 --- /dev/null +++ b/scripts/macos/entitlements.plist @@ -0,0 +1,6 @@ + + + + + + diff --git a/scripts/macos/notarize.sh b/scripts/macos/notarize.sh new file mode 100755 index 00000000..081f094d --- /dev/null +++ b/scripts/macos/notarize.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Submit a signed macOS binary to Apple's notary service via +# `xcrun notarytool`, wait for the verdict, and pull the rejection log +# if it fails. +# +# Usage: +# scripts/macos/notarize.sh +# +# Auth comes from the App Store Connect API key: +# APP_STORE_CONNECT_API_KEY_KEY_ID short alphanumeric key id +# APP_STORE_CONNECT_API_KEY_ISSUER_ID UUID-shaped issuer id +# APP_STORE_CONNECT_API_KEY_KEY PEM body (literal `\n` between +# lines accepted) +# +# notarytool only accepts `.zip`, `.pkg`, or `.dmg`, so the binary is +# ditto-zipped for submission. The notarization ticket is recorded +# against the binary's code-signature hash, so the raw binary passes +# Gatekeeper online lookups without stapling. + +binary="" + +usage() { + printf "usage: %s \n" "${0##*/}" +} + +while [ $# -gt 0 ]; do + case "$1" in + -h|--help) usage; exit 0 ;; + -*) printf >&2 "unknown arg: %s\n" "$1"; usage >&2; exit 1 ;; + *) + if [ -n "$binary" ]; then + printf >&2 "extra positional argument: %s\n" "$1"; usage >&2; exit 1 + fi + binary="$1"; shift ;; + esac +done + +if [ -z "$binary" ]; then + usage >&2; exit 1 +fi +if [ ! -f "$binary" ]; then + printf >&2 "binary not found: %s\n" "$binary"; exit 1 +fi + +key_id="${APP_STORE_CONNECT_API_KEY_KEY_ID-}" +issuer_id="${APP_STORE_CONNECT_API_KEY_ISSUER_ID-}" +key_pem="${APP_STORE_CONNECT_API_KEY_KEY-}" + +if [ -z "$key_id" ] || [ -z "$issuer_id" ] || [ -z "$key_pem" ]; then + printf >&2 "missing notarization auth: set APP_STORE_CONNECT_API_KEY_{KEY_ID,ISSUER_ID,KEY}\n" + exit 1 +fi + +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT + +p8="$work/AuthKey_${key_id}.p8" +# `%b` decodes literal `\n` escapes into real newlines. The trailing +# newline matters — notarytool rejects PEM without it as +# `invalidPEMDocument`. +printf '%b\n' "$key_pem" > "$p8" +chmod 600 "$p8" + +zip_path="$work/$(basename "$binary").zip" +printf "==> ditto-zipping %s for submission\n" "$binary" +ditto -c -k "$binary" "$zip_path" + +printf "==> submitting to notarytool (this can take a few minutes)\n" +submit_json="$work/submit.json" +xcrun notarytool submit "$zip_path" \ + --key "$p8" \ + --key-id "$key_id" \ + --issuer "$issuer_id" \ + --wait \ + --output-format json \ + > "$submit_json" + +cat "$submit_json" +printf "\n" + +status="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["status"])' "$submit_json")" +submission_id="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["id"])' "$submit_json")" + +if [ "$status" != "Accepted" ]; then + printf >&2 "==> notarization status: %s — fetching log\n" "$status" + xcrun notarytool log "$submission_id" \ + --key "$p8" --key-id "$key_id" --issuer "$issuer_id" + exit 1 +fi + +printf "==> notarization accepted (id=%s)\n" "$submission_id" diff --git a/scripts/macos/sign-and-notarize.sh b/scripts/macos/sign-and-notarize.sh new file mode 100755 index 00000000..ee0ad6f4 --- /dev/null +++ b/scripts/macos/sign-and-notarize.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Sign a macOS binary with Developer ID + hardened runtime, then +# submit it to Apple's notary service. +# +# Usage: +# scripts/macos/sign-and-notarize.sh +# [--team-id TEAM] +# [--entitlements FILE] +# [--identity 'Developer ID Application: ...'] +# [--skip-notarize] +# +# Entitlements default to `entitlements.plist` next to this script if +# it exists. `--skip-notarize` (or `FORKPRESS_SKIP_NOTARIZE=1`) signs +# but skips the notarytool round-trip. + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +team_id="" +identity="" +skip_notarize="${FORKPRESS_SKIP_NOTARIZE:-0}" +binary="" + +# macOS ships bash 3.2 where empty arrays plus `set -u` blow up on +# `"${arr[@]}"`. Build a single args array — starting from the +# always-present binary — instead of stitching optional sub-arrays +# together at the call site. +entitlements="$here/entitlements.plist" + +usage() { + printf "usage: %s [--team-id TEAM] [--entitlements FILE] [--identity IDENTITY] [--skip-notarize]\n" "${0##*/}" +} + +while [ $# -gt 0 ]; do + case "$1" in + --team-id) + [ $# -ge 2 ] || { printf >&2 "missing value for --team-id\n"; exit 1; } + team_id="$2"; shift 2 ;; + --entitlements) + [ $# -ge 2 ] || { printf >&2 "missing value for --entitlements\n"; exit 1; } + entitlements="$2"; shift 2 ;; + --identity) + [ $# -ge 2 ] || { printf >&2 "missing value for --identity\n"; exit 1; } + identity="$2"; shift 2 ;; + --skip-notarize) + skip_notarize=1; shift ;; + -h|--help) + usage; exit 0 ;; + -*) + printf >&2 "unknown arg: %s\n" "$1"; usage >&2; exit 1 ;; + *) + if [ -n "$binary" ]; then + printf >&2 "extra positional argument: %s\n" "$1"; usage >&2; exit 1 + fi + binary="$1"; shift ;; + esac +done + +if [ -z "$binary" ]; then + usage >&2; exit 1 +fi + +codesign_args=("$binary") +if [ -n "$team_id" ]; then + codesign_args+=(--team-id "$team_id") +fi +if [ -n "$entitlements" ] && [ -f "$entitlements" ]; then + codesign_args+=(--entitlements "$entitlements") +fi +if [ -n "$identity" ]; then + codesign_args+=(--identity "$identity") +fi + +"$here/codesign.sh" "${codesign_args[@]}" + +case "$skip_notarize" in + 1|true|yes|on) + printf "==> FORKPRESS_SKIP_NOTARIZE set — skipping notarytool submission\n" + exit 0 + ;; +esac + +"$here/notarize.sh" "$binary" From c73198871d8d23ac5e7c70820b75760e84355785 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Tue, 19 May 2026 21:33:42 +1000 Subject: [PATCH 2/4] Move codesign + notarize into fastlane lanes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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:` — 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:` — 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 --- .buildkite/commands/mac-aarch64-build.sh | 13 +--- .buildkite/commands/mac-x86_64-build.sh | 5 +- fastlane/.gitignore | 2 + fastlane/Fastfile | 59 +++++++++++++++ scripts/macos/codesign.sh | 87 ---------------------- scripts/macos/notarize.sh | 94 ------------------------ scripts/macos/sign-and-notarize.sh | 85 --------------------- 7 files changed, 67 insertions(+), 278 deletions(-) create mode 100644 fastlane/.gitignore delete mode 100755 scripts/macos/codesign.sh delete mode 100755 scripts/macos/notarize.sh delete mode 100755 scripts/macos/sign-and-notarize.sh diff --git a/.buildkite/commands/mac-aarch64-build.sh b/.buildkite/commands/mac-aarch64-build.sh index 0545d4b6..cb7bfa44 100755 --- a/.buildkite/commands/mac-aarch64-build.sh +++ b/.buildkite/commands/mac-aarch64-build.sh @@ -26,22 +26,17 @@ FORKPRESS_TARGET="$TARGET" scripts/build-dist.sh echo "--- :crab: cargo build --release forkpress ($TARGET)" cargo build --release --target "$TARGET" -p forkpress-cli --bin forkpress --locked -echo "--- :fastlane: Installing Developer ID cert via match" +echo "--- :fastlane: bundle install + cert install" bundle install bundle exec fastlane set_up_signing # Sign before the e2e so the test exercises the same hardened-runtime # binary the artifact upload ships. echo "--- :lock: Codesigning forkpress ($TARGET)" -scripts/macos/codesign.sh "target/$TARGET/release/forkpress" \ - --entitlements scripts/macos/entitlements.plist +bundle exec fastlane sign_binary binary:"target/$TARGET/release/forkpress" echo "--- :cow: COW strategy e2e (APFS sparsebundle)" FORKPRESS_FORCE_MACOS_APFS_SPARSEBUNDLE=1 tests/cow/e2e.sh "target/$TARGET/release/forkpress" -if [ "${FORKPRESS_SKIP_NOTARIZE:-0}" = "1" ]; then - echo "--- :fast_forward: FORKPRESS_SKIP_NOTARIZE=1 — skipping notarization" -else - echo "--- :apple: Notarizing forkpress ($TARGET)" - scripts/macos/notarize.sh "target/$TARGET/release/forkpress" -fi +echo "--- :apple: Notarizing forkpress ($TARGET)" +bundle exec fastlane notarize_binary binary:"target/$TARGET/release/forkpress" diff --git a/.buildkite/commands/mac-x86_64-build.sh b/.buildkite/commands/mac-x86_64-build.sh index 5e8972bf..d84a9327 100755 --- a/.buildkite/commands/mac-x86_64-build.sh +++ b/.buildkite/commands/mac-x86_64-build.sh @@ -22,7 +22,7 @@ FORKPRESS_RUNTIME_BUNDLE=/dev/null cargo build --release --target "$TARGET" -p f ls -lh "target/$TARGET/release/forkpress" file "target/$TARGET/release/forkpress" || true -echo "--- :fastlane: Installing Developer ID cert via match" +echo "--- :fastlane: bundle install + cert install" bundle install bundle exec fastlane set_up_signing @@ -31,5 +31,4 @@ bundle exec fastlane set_up_signing # /dev/null above) and is unrunnable, so notarizing it would waste the # notary API quota on a non-shippable artifact. echo "--- :lock: Codesigning forkpress ($TARGET)" -scripts/macos/codesign.sh "target/$TARGET/release/forkpress" \ - --entitlements scripts/macos/entitlements.plist +bundle exec fastlane sign_binary binary:"target/$TARGET/release/forkpress" diff --git a/fastlane/.gitignore b/fastlane/.gitignore new file mode 100644 index 00000000..56e5e3de --- /dev/null +++ b/fastlane/.gitignore @@ -0,0 +1,2 @@ +README.md +report.xml diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 2aa048fa..f43906e1 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -2,7 +2,9 @@ UI.user_error!('Please run fastlane via `bundle exec`') unless FastlaneCore::Helper.bundler? +REPO_ROOT = File.expand_path('..', __dir__) APPLE_TEAM_ID = 'PZYM8XX95Q' +ENTITLEMENTS_PATH = File.join(REPO_ROOT, 'scripts/macos/entitlements.plist') CODE_SIGNING_STORAGE_OPTIONS = { storage_mode: 's3', @@ -46,3 +48,60 @@ lane :set_up_signing do |readonly: true| **CODE_SIGNING_STORAGE_OPTIONS ) end + +desc 'Codesign a macOS binary with Developer ID + hardened runtime' +lane :sign_binary do |binary:| + # Fastlane runs `sh` from the `fastlane/` directory; resolve the + # caller's `binary:` argument against the repo root so the + # cargo output path (`target/...`) makes sense to codesign. + binary = File.expand_path(binary, REPO_ROOT) + + identity = `security find-identity -v -p codesigning | awk -v team='(#{APPLE_TEAM_ID})' '/Developer ID Application:/ && index($0, team) { sub(/^[^"]*"/, ""); sub(/"[^"]*$/, ""); print; exit }'`.strip + UI.user_error!("No Developer ID Application identity for team #{APPLE_TEAM_ID} in keychain") if identity.empty? + + args = ['codesign', '--force', '--options', 'runtime', '--timestamp', '--sign', identity] + args += ['--entitlements', ENTITLEMENTS_PATH] if File.file?(ENTITLEMENTS_PATH) + args << binary + + UI.message("Codesigning #{binary} as #{identity}") + sh(*args) + sh('codesign', '--verify', '--strict', '--verbose=2', binary) +end + +desc 'Submit a signed macOS binary to Apple notary service' +lane :notarize_binary do |binary:| + binary = File.expand_path(binary, REPO_ROOT) + EnvManager.require_env_vars!(*ASC_API_KEY_ENV_VARS) + + # notarytool only accepts .zip / .pkg / .dmg; ditto-zip the raw Mach-O. + # Stapling is skipped because the ticket can't be attached to a raw + # binary anyway — Gatekeeper resolves it online via the binary's + # code-signature hash. + zip_path = "#{binary}-notarize.zip" + sh('ditto', '-c', '-k', binary, zip_path) + begin + # `bundle_id` is required when the package is a raw .zip — fastlane's + # notarize action otherwise tries to read it from an `.app/Info.plist` + # inside and errors out. notarytool itself only uses it as a label on + # the submission record, so any stable identifier works. + # + # `verbose: false` is load-bearing: with `use_notarytool: true`, + # fastlane invokes notarytool with `--output-format json` AND + # `--verbose`, which interleaves debug lines with the JSON document + # and then crashes the action's `JSON.parse` on the submit response + # — even though notarization itself succeeded. We keep + # `print_log: true` so the action still pulls the diagnostic log on + # `Invalid`/`Rejected` status. + notarize( + package: zip_path, + use_notarytool: true, + api_key: app_store_connect_api_key, + bundle_id: 'com.automattic.forkpress', + skip_stapling: true, + print_log: true, + verbose: false + ) + ensure + File.delete(zip_path) if File.exist?(zip_path) + end +end diff --git a/scripts/macos/codesign.sh b/scripts/macos/codesign.sh deleted file mode 100755 index 774ebdfc..00000000 --- a/scripts/macos/codesign.sh +++ /dev/null @@ -1,87 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -# Codesign a macOS binary with a Developer ID Application identity + -# hardened runtime + secure timestamp. -# -# Usage: -# scripts/macos/codesign.sh -# [--team-id TEAM] -# [--entitlements FILE] -# [--identity 'Developer ID Application: ...'] -# -# Identity is resolved from the codesigning keychain by team id. -# FORKPRESS_CODESIGN_IDENTITY overrides the lookup. - -team_id="PZYM8XX95Q" -entitlements="" -identity="${FORKPRESS_CODESIGN_IDENTITY:-}" -binary="" - -usage() { - printf "usage: %s [--team-id TEAM] [--entitlements FILE] [--identity IDENTITY]\n" "${0##*/}" -} - -while [ $# -gt 0 ]; do - case "$1" in - --team-id) - [ $# -ge 2 ] || { printf >&2 "missing value for --team-id\n"; exit 1; } - team_id="$2"; shift 2 ;; - --entitlements) - [ $# -ge 2 ] || { printf >&2 "missing value for --entitlements\n"; exit 1; } - entitlements="$2"; shift 2 ;; - --identity) - [ $# -ge 2 ] || { printf >&2 "missing value for --identity\n"; exit 1; } - identity="$2"; shift 2 ;; - -h|--help) - usage; exit 0 ;; - -*) - printf >&2 "unknown arg: %s\n" "$1"; usage >&2; exit 1 ;; - *) - if [ -n "$binary" ]; then - printf >&2 "extra positional argument: %s\n" "$1"; usage >&2; exit 1 - fi - binary="$1"; shift ;; - esac -done - -if [ -z "$binary" ]; then - usage >&2; exit 1 -fi -if [ ! -f "$binary" ]; then - printf >&2 "binary not found: %s\n" "$binary"; exit 1 -fi - -if [ -z "$identity" ]; then - identity="$(security find-identity -v -p codesigning | awk -v team="(${team_id})" ' - /Developer ID Application:/ && index($0, team) { - sub(/^[^"]*"/, "") - sub(/"[^"]*$/, "") - print - exit - } - ')" -fi -if [ -z "$identity" ]; then - printf >&2 "no Developer ID Application identity for team %s in keychain\n" "$team_id" - printf >&2 "(set FORKPRESS_CODESIGN_IDENTITY=... or pass --identity to override)\n" - exit 1 -fi - -printf "==> codesigning %s\n identity: %s\n" "$binary" "$identity" - -codesign_args=(--force --options runtime --timestamp --sign "$identity") -if [ -n "$entitlements" ]; then - if [ ! -f "$entitlements" ]; then - printf >&2 "entitlements file not found: %s\n" "$entitlements"; exit 1 - fi - codesign_args+=(--entitlements "$entitlements") -fi - -codesign "${codesign_args[@]}" "$binary" - -printf "==> verifying signature\n" -codesign --verify --strict --verbose=2 "$binary" -codesign --display --verbose=2 "$binary" 2>&1 \ - | grep -E 'Authority|TeamIdentifier|Signature|flags|Identifier' || true diff --git a/scripts/macos/notarize.sh b/scripts/macos/notarize.sh deleted file mode 100755 index 081f094d..00000000 --- a/scripts/macos/notarize.sh +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -# Submit a signed macOS binary to Apple's notary service via -# `xcrun notarytool`, wait for the verdict, and pull the rejection log -# if it fails. -# -# Usage: -# scripts/macos/notarize.sh -# -# Auth comes from the App Store Connect API key: -# APP_STORE_CONNECT_API_KEY_KEY_ID short alphanumeric key id -# APP_STORE_CONNECT_API_KEY_ISSUER_ID UUID-shaped issuer id -# APP_STORE_CONNECT_API_KEY_KEY PEM body (literal `\n` between -# lines accepted) -# -# notarytool only accepts `.zip`, `.pkg`, or `.dmg`, so the binary is -# ditto-zipped for submission. The notarization ticket is recorded -# against the binary's code-signature hash, so the raw binary passes -# Gatekeeper online lookups without stapling. - -binary="" - -usage() { - printf "usage: %s \n" "${0##*/}" -} - -while [ $# -gt 0 ]; do - case "$1" in - -h|--help) usage; exit 0 ;; - -*) printf >&2 "unknown arg: %s\n" "$1"; usage >&2; exit 1 ;; - *) - if [ -n "$binary" ]; then - printf >&2 "extra positional argument: %s\n" "$1"; usage >&2; exit 1 - fi - binary="$1"; shift ;; - esac -done - -if [ -z "$binary" ]; then - usage >&2; exit 1 -fi -if [ ! -f "$binary" ]; then - printf >&2 "binary not found: %s\n" "$binary"; exit 1 -fi - -key_id="${APP_STORE_CONNECT_API_KEY_KEY_ID-}" -issuer_id="${APP_STORE_CONNECT_API_KEY_ISSUER_ID-}" -key_pem="${APP_STORE_CONNECT_API_KEY_KEY-}" - -if [ -z "$key_id" ] || [ -z "$issuer_id" ] || [ -z "$key_pem" ]; then - printf >&2 "missing notarization auth: set APP_STORE_CONNECT_API_KEY_{KEY_ID,ISSUER_ID,KEY}\n" - exit 1 -fi - -work="$(mktemp -d)" -trap 'rm -rf "$work"' EXIT - -p8="$work/AuthKey_${key_id}.p8" -# `%b` decodes literal `\n` escapes into real newlines. The trailing -# newline matters — notarytool rejects PEM without it as -# `invalidPEMDocument`. -printf '%b\n' "$key_pem" > "$p8" -chmod 600 "$p8" - -zip_path="$work/$(basename "$binary").zip" -printf "==> ditto-zipping %s for submission\n" "$binary" -ditto -c -k "$binary" "$zip_path" - -printf "==> submitting to notarytool (this can take a few minutes)\n" -submit_json="$work/submit.json" -xcrun notarytool submit "$zip_path" \ - --key "$p8" \ - --key-id "$key_id" \ - --issuer "$issuer_id" \ - --wait \ - --output-format json \ - > "$submit_json" - -cat "$submit_json" -printf "\n" - -status="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["status"])' "$submit_json")" -submission_id="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["id"])' "$submit_json")" - -if [ "$status" != "Accepted" ]; then - printf >&2 "==> notarization status: %s — fetching log\n" "$status" - xcrun notarytool log "$submission_id" \ - --key "$p8" --key-id "$key_id" --issuer "$issuer_id" - exit 1 -fi - -printf "==> notarization accepted (id=%s)\n" "$submission_id" diff --git a/scripts/macos/sign-and-notarize.sh b/scripts/macos/sign-and-notarize.sh deleted file mode 100755 index ee0ad6f4..00000000 --- a/scripts/macos/sign-and-notarize.sh +++ /dev/null @@ -1,85 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -# Sign a macOS binary with Developer ID + hardened runtime, then -# submit it to Apple's notary service. -# -# Usage: -# scripts/macos/sign-and-notarize.sh -# [--team-id TEAM] -# [--entitlements FILE] -# [--identity 'Developer ID Application: ...'] -# [--skip-notarize] -# -# Entitlements default to `entitlements.plist` next to this script if -# it exists. `--skip-notarize` (or `FORKPRESS_SKIP_NOTARIZE=1`) signs -# but skips the notarytool round-trip. - -here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -team_id="" -identity="" -skip_notarize="${FORKPRESS_SKIP_NOTARIZE:-0}" -binary="" - -# macOS ships bash 3.2 where empty arrays plus `set -u` blow up on -# `"${arr[@]}"`. Build a single args array — starting from the -# always-present binary — instead of stitching optional sub-arrays -# together at the call site. -entitlements="$here/entitlements.plist" - -usage() { - printf "usage: %s [--team-id TEAM] [--entitlements FILE] [--identity IDENTITY] [--skip-notarize]\n" "${0##*/}" -} - -while [ $# -gt 0 ]; do - case "$1" in - --team-id) - [ $# -ge 2 ] || { printf >&2 "missing value for --team-id\n"; exit 1; } - team_id="$2"; shift 2 ;; - --entitlements) - [ $# -ge 2 ] || { printf >&2 "missing value for --entitlements\n"; exit 1; } - entitlements="$2"; shift 2 ;; - --identity) - [ $# -ge 2 ] || { printf >&2 "missing value for --identity\n"; exit 1; } - identity="$2"; shift 2 ;; - --skip-notarize) - skip_notarize=1; shift ;; - -h|--help) - usage; exit 0 ;; - -*) - printf >&2 "unknown arg: %s\n" "$1"; usage >&2; exit 1 ;; - *) - if [ -n "$binary" ]; then - printf >&2 "extra positional argument: %s\n" "$1"; usage >&2; exit 1 - fi - binary="$1"; shift ;; - esac -done - -if [ -z "$binary" ]; then - usage >&2; exit 1 -fi - -codesign_args=("$binary") -if [ -n "$team_id" ]; then - codesign_args+=(--team-id "$team_id") -fi -if [ -n "$entitlements" ] && [ -f "$entitlements" ]; then - codesign_args+=(--entitlements "$entitlements") -fi -if [ -n "$identity" ]; then - codesign_args+=(--identity "$identity") -fi - -"$here/codesign.sh" "${codesign_args[@]}" - -case "$skip_notarize" in - 1|true|yes|on) - printf "==> FORKPRESS_SKIP_NOTARIZE set — skipping notarytool submission\n" - exit 0 - ;; -esac - -"$here/notarize.sh" "$binary" From 148beb0f717bbcb4445b8347f6fd38be1698d090 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Wed, 20 May 2026 11:01:44 +1000 Subject: [PATCH 3/4] DRY mac BK fastlane setup into a sourced helper Both `mac-aarch64-build.sh` and `mac-x86_64-build.sh` repeated the same three-line dance to resolve fastlane gem deps and install the Developer ID cert into a temp keychain. Extract it into `_lib/setup-fastlane.sh` following the existing `_lib/` pattern. --- Generated with the help of Claude Code, https://claude.ai/code Co-Authored-By: Claude Code Opus 4.7 --- .buildkite/commands/_lib/setup-fastlane.sh | 9 +++++++++ .buildkite/commands/mac-aarch64-build.sh | 5 ++--- .buildkite/commands/mac-x86_64-build.sh | 5 ++--- 3 files changed, 13 insertions(+), 6 deletions(-) create mode 100755 .buildkite/commands/_lib/setup-fastlane.sh diff --git a/.buildkite/commands/_lib/setup-fastlane.sh b/.buildkite/commands/_lib/setup-fastlane.sh new file mode 100755 index 00000000..c51112ab --- /dev/null +++ b/.buildkite/commands/_lib/setup-fastlane.sh @@ -0,0 +1,9 @@ +# Resolve fastlane gem deps and install the Automattic Developer ID +# Application cert into a temp keychain via the `set_up_signing` lane. +# +# Usage: +# source .buildkite/commands/_lib/setup-fastlane.sh + +echo "--- :fastlane: bundle install + cert install" +bundle install +bundle exec fastlane set_up_signing diff --git a/.buildkite/commands/mac-aarch64-build.sh b/.buildkite/commands/mac-aarch64-build.sh index cb7bfa44..7768ed11 100755 --- a/.buildkite/commands/mac-aarch64-build.sh +++ b/.buildkite/commands/mac-aarch64-build.sh @@ -26,9 +26,8 @@ FORKPRESS_TARGET="$TARGET" scripts/build-dist.sh echo "--- :crab: cargo build --release forkpress ($TARGET)" cargo build --release --target "$TARGET" -p forkpress-cli --bin forkpress --locked -echo "--- :fastlane: bundle install + cert install" -bundle install -bundle exec fastlane set_up_signing +# shellcheck source=_lib/setup-fastlane.sh +source "$(dirname "$0")/_lib/setup-fastlane.sh" # Sign before the e2e so the test exercises the same hardened-runtime # binary the artifact upload ships. diff --git a/.buildkite/commands/mac-x86_64-build.sh b/.buildkite/commands/mac-x86_64-build.sh index d84a9327..6e836892 100755 --- a/.buildkite/commands/mac-x86_64-build.sh +++ b/.buildkite/commands/mac-x86_64-build.sh @@ -22,9 +22,8 @@ FORKPRESS_RUNTIME_BUNDLE=/dev/null cargo build --release --target "$TARGET" -p f ls -lh "target/$TARGET/release/forkpress" file "target/$TARGET/release/forkpress" || true -echo "--- :fastlane: bundle install + cert install" -bundle install -bundle exec fastlane set_up_signing +# shellcheck source=_lib/setup-fastlane.sh +source "$(dirname "$0")/_lib/setup-fastlane.sh" # Sign as a smoke check on the codesign chain, but skip notarization — # this binary has no embedded runtime (built with FORKPRESS_RUNTIME_BUNDLE= From 9763d4665c389876aea79cfb696040a9fa8a5f80 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Wed, 20 May 2026 12:58:08 +1000 Subject: [PATCH 4/4] Honor `FORKPRESS_SKIP_NOTARIZE` in mac aarch64 build The PR description advertised the escape hatch but the script invoked `fastlane notarize_binary` unconditionally. Gate the notarize step so branches without App Store Connect creds (or developers iterating locally) can opt out and still produce a signed binary. --- Generated with the help of Claude Code, https://code.claude.com Co-Authored-By: Claude Opus 4.7 (1M context) --- .buildkite/commands/mac-aarch64-build.sh | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.buildkite/commands/mac-aarch64-build.sh b/.buildkite/commands/mac-aarch64-build.sh index 7768ed11..75f73354 100755 --- a/.buildkite/commands/mac-aarch64-build.sh +++ b/.buildkite/commands/mac-aarch64-build.sh @@ -37,5 +37,12 @@ bundle exec fastlane sign_binary binary:"target/$TARGET/release/forkpress" echo "--- :cow: COW strategy e2e (APFS sparsebundle)" FORKPRESS_FORCE_MACOS_APFS_SPARSEBUNDLE=1 tests/cow/e2e.sh "target/$TARGET/release/forkpress" -echo "--- :apple: Notarizing forkpress ($TARGET)" -bundle exec fastlane notarize_binary binary:"target/$TARGET/release/forkpress" +# `FORKPRESS_SKIP_NOTARIZE=1` opts out of the ~minutes-long notary +# round-trip and lets branches without App Store Connect creds in scope +# still produce a signed (un-notarized) binary. +if [ "${FORKPRESS_SKIP_NOTARIZE:-0}" = "1" ]; then + echo "--- :apple: Skipping notarization (FORKPRESS_SKIP_NOTARIZE=1)" +else + echo "--- :apple: Notarizing forkpress ($TARGET)" + bundle exec fastlane notarize_binary binary:"target/$TARGET/release/forkpress" +fi