Skip to content

add local script to sign and notarize imessage-cli#70

Open
mokagio wants to merge 7 commits intomainfrom
ainfra-2351-sign-and-notarize-imessage-cli-in-ci-local
Open

add local script to sign and notarize imessage-cli#70
mokagio wants to merge 7 commits intomainfrom
ainfra-2351-sign-and-notarize-imessage-cli-in-ci-local

Conversation

@mokagio
Copy link
Copy Markdown

@mokagio mokagio commented May 6, 2026

Rationale

First half of AINFRA-2351 — adds the local automation that builds, signs (Developer ID, hardened runtime), and notarizes imessage-cli end 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

  • strip happens before codesign — stripping a signed binary invalidates the signature.
  • Universal binary by default; --arch arm64|x86_64|universal flag for single-arch (default takes ~7min cold; single-arch ~4min).
  • notarytool's PEM parser requires a trailing newline, otherwise rejects with invalidPEMDocument (OpenSSL accepts it without).

How to test

# from a Mac with the team's Developer ID Application cert in keychain
# and APP_STORE_CONNECT_API_KEY_* env vars set:
./scripts/sign-and-notarize-cli                       # default: universal
./scripts/sign-and-notarize-cli --arch arm64           # single arch

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.

mokagio and others added 3 commits May 6, 2026 10:37
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>
@mokagio mokagio self-assigned this May 6, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 6, 2026

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 72fd2e17-e1ca-4398-84bd-0c95899bc86f

📥 Commits

Reviewing files that changed from the base of the PR and between c3800d8 and 00ea41c.

📒 Files selected for processing (1)
  • scripts/sign-and-notarize-cli
📜 Recent review details
🔇 Additional comments (4)
scripts/sign-and-notarize-cli (4)

12-50: Argument parsing and arch validation are solid.

The missing-value guards and explicit --arch allowlist make CLI behavior predictable and fail-fast.


64-83: Dynamic signing identity resolution is a strong fix.

Resolving Developer ID from keychain by team and allowing IDENTITY override removes the previous hardcoded-org coupling cleanly.


111-126: Signing order and verification look correct.

Stripping before codesign, then running strict verify/display checks, is the right sequence for a reproducible signed artifact.


131-165: Notarization flow and failure handling look good.

PEM decoding/newline handling, --wait JSON output parsing, and fetching logs on non-accepted status provide robust operational feedback.


📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • Chores
    • Added macOS code signing and notarization support for the CLI tool, enabling properly signed builds for both ARM64 and x86_64 architectures.

Walkthrough

Adds 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.

Changes

Signing & Notarization Flow

Layer / File(s) Summary
Data / Entitlements
scripts/imessage-cli.entitlements
New XML plist enabling com.apple.security.automation.apple-events = true.
CLI / Flags & Validation
scripts/sign-and-notarize-cli
Adds entrypoint, --arch and --team-id parsing, validation, defaults, and help/unknown-arg handling.
Keychain / Identity Resolution
scripts/sign-and-notarize-cli
Reads App Store Connect API env vars and resolves Developer ID codesign identity from keychain with optional IDENTITY override; errors if not found.
Build Steps
scripts/sign-and-notarize-cli
Defines build helpers, builds per-arch or both, and creates universal binary via lipo for universal option.
Strip & Codesign
scripts/sign-and-notarize-cli, scripts/imessage-cli.entitlements
Strips debug symbols, codesigns binary with hardened runtime, timestamp, and entitlements, and verifies signature metadata.
Workspace / PEM Handling
scripts/sign-and-notarize-cli
Creates temporary workspace with cleanup trap, writes .p8 Notary key from env (handles escaped newlines), and tightens permissions.
Packaging & Notarization Submit
scripts/sign-and-notarize-cli
Zips signed binary, submits via xcrun notarytool with key-id/issuer and .p8, prints submission JSON, parses status/id, and fetches notarization log on non-Accepted status.
Output / Exit Handling
scripts/sign-and-notarize-cli
Exits non-zero on notarization failure after fetching log; prints notarization id and signed binary path on success.

Sequence Diagram

sequenceDiagram
  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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main change: adding a local script to sign and notarize the imessage-cli binary, which matches the primary functionality of the new files.
Description check ✅ Passed The description is directly related to the changeset, providing rationale, implementation details, gotchas, and testing instructions for the signing and notarization script being added.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ainfra-2351-sign-and-notarize-imessage-cli-in-ci-local

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.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between c2f552e and 89f8ca7.

📒 Files selected for processing (2)
  • scripts/imessage-cli.entitlements
  • 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 (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.

Comment thread scripts/sign-and-notarize-cli
Comment thread scripts/sign-and-notarize-cli
mokagio and others added 2 commits May 6, 2026 11:40
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>
@mokagio
Copy link
Copy Markdown
Author

mokagio commented May 6, 2026

Addressed both CodeRabbit comments.

  • 61a28eb — guards --arch and --team-id against missing values so a bare --arch no longer trips set -u's unbound-variable error; prints usage to stderr and exits 1.
  • dfe6c5b — drops the hardcoded Automattic, Inc. org name and resolves the full Developer ID identity from security find-identity by team id at runtime. IDENTITY env var bypasses the lookup if you ever need to force a specific cert.

Re-ran the script end-to-end with --arch arm64 after the second fix; notarization id 57ac6793-9b65-4f27-854b-37c082a956c5 was accepted.


Posted by Claude Code (Opus 4.7, 1M context) on behalf of @mokagio with approval.

@indent
Copy link
Copy Markdown

indent Bot commented May 6, 2026

It looks like you're not a member of this organization on Indent. To use Indent, visit app.indent.com to get started.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
scripts/sign-and-notarize-cli (1)

158-159: 💤 Low value

Consider 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

📥 Commits

Reviewing files that changed from the base of the PR and between 89f8ca7 and dfe6c5b.

📒 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 pipefail for strict error handling. Defaults and usage function are clean.


12-42: Argument parsing guards are now in place.

The $# -lt 2 checks before accessing $2 prevent unbound variable errors under set -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 with set -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 an IDENTITY env 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 600 on the PEM key file, and clear documentation of the \n decoding 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>
@mokagio mokagio marked this pull request as ready for review May 7, 2026 05:36
@mokagio mokagio requested review from iangmaia and twstokes May 7, 2026 05:36
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>
Copilot AI review requested due to automatic review settings May 7, 2026 05:44
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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-cli Bash script to build (optionally universal), strip, Developer ID sign (hardened runtime), and notarize imessage-cli via notarytool.
  • 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.

Comment on lines +102 to +110
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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants