add local script to sign and notarize imessage-cli#70
Conversation
First step toward AINFRA-2351 (sign and notarize `imessage-cli` in CI).
Validates entitlements, signing identity, and notarytool credentials end
to end on a workstation before wiring the same flow into
`release-imessage-cli.yml`.
Entitlements are minimal — just `com.apple.security.automation.apple-events`,
required so hardened runtime lets the CLI drive Messages.app. TCC governs
Accessibility / Contacts / Full Disk Access, so no entitlements there.
The script reads App Store Connect API key creds from team-keyed env vars
(`APP_STORE_CONNECT_API_KEY_<TEAM>_{KEY_ID,ISSUER_ID,KEY}`), defaulting
to team `PZYM8XX95Q`.
Gotchas worth remembering when porting to CI:
- Env-var PEM stores `\n` as literal backslash-n; need `printf '%b'` to
decode, not `printf '%s'`.
- notarytool's PEM parser rejects `-----END PRIVATE KEY-----` without a
trailing newline as `invalidPEMDocument`. OpenSSL accepts it. Always
emit a final `\n`.
- `eval printf '%s' "$VAR"` word-splits on whitespace and `printf`
concatenates without separators — `BEGIN PRIVATE KEY` becomes
`BEGINPRIVATEKEY`. Use bash indirect expansion `${!var}` instead.
---
Generated with the help of Claude Code, https://claude.ai/code
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`strip` invalidates an existing signature, so it has to happen *before* `codesign`, not after. Halves the release artifact (10.6MB -> 5.3MB on arm64) — matches the intent of the bare `strip` step in `release-imessage-cli.yml`, which currently runs unsigned. --- Generated with the help of Claude Code, https://claude.ai/code Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the universal/x86 half of the open todo on `todos.md:28`. The script now builds both arm64 and x86_64 slices, lipos them into one fat binary, and signs/notarizes the result with a single signature that covers both arches. Adds `--arch arm64|x86_64|universal` and `--team-id TEAM` flags; defaults are universal + `PZYM8XX95Q`. Single-arch is still available for shaking out signing/credential issues without paying the second build (~3min on a clean cache). Wall-clock on a clean cache ended up ~6:51 (arm64 218s + x86_64 173s + sign/zip/notary ~30-90s). Incremental relink is sub-10s for both slices combined. --- Generated with the help of Claude Code, https://claude.ai/code Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📜 Recent review details🔇 Additional comments (4)
📝 WalkthroughSummary by CodeRabbitRelease Notes
WalkthroughAdds an entitlements plist enabling Apple Events automation and a new signing/notarization shell script that builds imessage-cli (single-arch or universal), codesigns it with entitlements, and submits the package to Apple Notary using App Store Connect API credentials, handling logs and exit status. ChangesSigning & Notarization Flow
Sequence DiagramsequenceDiagram
autonumber
participant Dev as Developer
participant Repo as Repository (scripts)
participant Build as Swift Build System
participant Keychain as Codesign / Keychain
participant Notary as Apple Notary (notarytool)
Dev->>Repo: invoke `scripts/sign-and-notarize-cli --arch --team-id`
Repo->>Build: run `swift build` per-arch (arm64/x86_64) or single-arch
Build-->>Repo: produced binary(ies)
Repo->>Keychain: resolve identity, strip symbols, codesign binary with entitlements
Keychain-->>Repo: verified signed binary
Repo->>Repo: write PEM from env, zip signed binary
Repo->>Notary: submit ZIP via `notarytool` (key-id, issuer, PEM)
Notary-->>Repo: return submission JSON (status, id)
alt status != Accepted
Repo->>Notary: fetch notarization log
Notary-->>Repo: log -> Repo exits non-zero
else status == Accepted
Repo-->>Dev: report notarization id and signed binary path
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@scripts/sign-and-notarize-cli`:
- Around line 10-16: Guard the --arch and --team-id case blocks against missing
values by checking the argument count before reading $2; in the case labels for
--arch and --team-id (the branches that set arch="$2" and team_id="$2") add an
if [ $# -lt 2 ] check that prints the script usage/help and exits non‑zero,
otherwise proceed to assign and shift 2, so invoking the script with those flags
but no value does not access an unbound $2 when set -u is enabled.
- Around line 14-16: The --team-id flag currently only sets team_id but codesign
still uses a hardcoded certificate subject, so non-Automattic teams fail; update
the script so the supplied value either (a) accepts a full signing identity
string and assigns it to the SIGNING_IDENTITY variable used by the codesign
invocation, or (b) uses the provided team_id to look up the correct identity via
the macOS keychain (security find-identity / security find-certificate) and set
SIGNING_IDENTITY before the codesign call; ensure the help text matches the
chosen behavior and replace the hardcoded certificate subject in the codesign
invocation with the SIGNING_IDENTITY variable.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 12a29851-6ecb-4d34-a44a-2e55558aef67
📒 Files selected for processing (2)
scripts/imessage-cli.entitlementsscripts/sign-and-notarize-cli
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (1)
scripts/imessage-cli.entitlements (1)
5-6: Scoped entitlement looks right.Keeping this to just the Apple Events entitlement is the right shape for a hardened-runtime CLI that needs Messages automation.
Without the guard, `set -u` makes a bare `--arch` or `--team-id` blow up on `$2` with a cryptic "unbound variable" instead of printing usage. CodeRabbit on PR #70. --- Generated with the help of Claude Code, https://claude.ai/code Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The hardcoded `Developer ID Application: Automattic, Inc. (...)` only worked for Automattic certs, so `--team-id` against a non-Automattic team would silently fall back to a misleading codesign error. Look up the full identity string from `security find-identity` instead, matching by team id, with `IDENTITY` env override for edge cases. CodeRabbit on PR #70. --- Generated with the help of Claude Code, https://claude.ai/code Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Addressed both CodeRabbit comments.
Re-ran the script end-to-end with Posted by Claude Code (Opus 4.7, 1M context) on behalf of @mokagio with approval. |
|
It looks like you're not a member of this organization on Indent. To use Indent, visit app.indent.com to get started. |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
scripts/sign-and-notarize-cli (1)
158-159: 💤 Low valueConsider consolidating JSON parsing into a single python3 call.
Minor efficiency improvement: you could extract both values in one invocation to avoid parsing the JSON twice.
♻️ Optional: single-pass JSON extraction
-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")" +read -r status submission_id < <(python3 -c ' +import json, sys +d = json.load(open(sys.argv[1])) +print(d["status"], d["id"]) +' "$submit_json")🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/sign-and-notarize-cli` around lines 158 - 159, Replace the two separate python3 calls that set status and submission_id by a single python3 invocation that parses "$submit_json" once and prints both values (e.g., space- or newline-separated), then capture those two outputs into the shell variables status and submission_id with a single read; update the lines that reference status and submission_id accordingly so they now come from that single parse operation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@scripts/sign-and-notarize-cli`:
- Around line 158-159: Replace the two separate python3 calls that set status
and submission_id by a single python3 invocation that parses "$submit_json" once
and prints both values (e.g., space- or newline-separated), then capture those
two outputs into the shell variables status and submission_id with a single
read; update the lines that reference status and submission_id accordingly so
they now come from that single parse operation.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 064485a9-033b-4f4e-b9e6-a4da2cdb002b
📒 Files selected for processing (1)
scripts/sign-and-notarize-cli
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (7)
scripts/sign-and-notarize-cli (7)
1-11: LGTM!Good use of
set -euo pipefailfor strict error handling. Defaults and usage function are clean.
12-42: Argument parsing guards are now in place.The
$# -lt 2checks before accessing$2prevent unbound variable errors underset -u. This addresses the previous review feedback.
44-63: LGTM!Architecture validation is correct, and the indirect variable expansion pattern (
${!var_name-}) safely retrieves team-keyed environment variables while remaining compatible withset -u.
68-86: Identity resolution from keychain is now implemented.The script correctly resolves the codesigning identity by team ID using
security find-identity, with anIDENTITYenv var escape hatch. This addresses the previous review feedback about the hardcoded certificate subject.
88-130: Build and signing flow is well-structured.The ordering of strip → codesign is correct and documented. The codesign flags (
--options runtime,--timestamp,--entitlements) are appropriate for hardened runtime signing required by notarization.
132-144: LGTM!Good security hygiene: temp directory with cleanup trap,
chmod 600on the PEM key file, and clear documentation of the\ndecoding requirement.
161-169: LGTM!Good error handling: fetching the notarization log on failure provides actionable debugging information.
Reads `APP_STORE_CONNECT_API_KEY_{KEY_ID,ISSUER_ID,KEY}` first (the
canonical Fastlane convention, and what the Buildkite secrets surface
to the agent), then falls back to the existing team-id-keyed names
(`APP_STORE_CONNECT_API_KEY_<TEAM>_{...}`) so local shells holding
creds for multiple teams in parallel keep working.
The error message now lists both forms so future-me doesn't waste
time wondering which env var the script wants.
---
Generated with the help of Claude Code, https://claude.ai/code
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
That fallback existed because my local shell holds creds for multiple
teams under `APP_STORE_CONNECT_API_KEY_<TEAM>_{...}` names, but it's
a quirk of my setup, not something the script should carry weight for.
CI uses the canonical `APP_STORE_CONNECT_API_KEY_{KEY_ID,ISSUER_ID,KEY}`
names directly. If a multi-team workflow becomes a real need later,
we can reintroduce it then.
`--team-id` is still used for the keychain identity lookup.
---
Generated with the help of Claude Code, https://claude.ai/code
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a local, end-to-end macOS signing + notarization workflow for the imessage-cli Swift executable, intended to be exercised on a developer workstation before wiring the same flow into CI/release automation.
Changes:
- Introduces a
scripts/sign-and-notarize-cliBash script to build (optionally universal), strip, Developer ID sign (hardened runtime), and notarizeimessage-clivianotarytool. - Adds an entitlements plist enabling Apple Events automation for the signed CLI binary.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| scripts/sign-and-notarize-cli | New local build/sign/notarize script with arch selection and App Store Connect API key handling. |
| scripts/imessage-cli.entitlements | New entitlements plist (Apple Events automation) used during codesigning. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| mkdir -p "$out_dir" | ||
| binary="$out_dir/imessage-cli" | ||
| printf "==> creating universal binary\n" | ||
| lipo -create "$arm64_bin" "$x86_bin" -output "$binary" | ||
| else | ||
| build_arch "$arch" | ||
| binary="$(bin_for_arch "$arch")/imessage-cli" | ||
| fi | ||
|
|
Rationale
First half of AINFRA-2351 — adds the local automation that builds, signs (Developer ID, hardened runtime), and notarizes
imessage-cliend to end.CI integration is intentionally out of scope here so the signing pipeline can be exercised on a workstation before being wired into a release pipeline. See #71 and #72
Gotchas
striphappens beforecodesign— stripping a signed binary invalidates the signature.--arch arm64|x86_64|universalflag for single-arch (default takes ~7min cold; single-arch ~4min).invalidPEMDocument(OpenSSL accepts it without).How to test
Produces a Developer ID-signed, hardened-runtime, Apple-notarized universal binary at
.build/universal/release/imessage-cli.End-to-end was validated locally during development (notarization id
5a725b81-b2e7-4212-b0a3-cc8dfe794e97).Posted by Claude Code (Opus 4.7, 1M context) on behalf of @mokagio with approval.