Skip to content

Commit a0d67db

Browse files
committed
Add CI and macOS signing workflow
1 parent 93c1a66 commit a0d67db

8 files changed

Lines changed: 352 additions & 14 deletions

File tree

.github/workflows/ci.yml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: CI
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches:
7+
- main
8+
9+
jobs:
10+
test:
11+
name: Python, Web, and macOS checks
12+
runs-on: macos-latest
13+
timeout-minutes: 20
14+
steps:
15+
- name: Checkout
16+
uses: actions/checkout@v4
17+
18+
- name: Install uv
19+
uses: astral-sh/setup-uv@v5
20+
21+
- name: Install Python dependencies
22+
run: uv sync --extra dev
23+
24+
- name: Run Python tests
25+
run: |
26+
uv run python -m unittest discover -s tests
27+
uv run pytest -q
28+
29+
- name: Compile Python modules
30+
run: uv run python -m compileall -q aear harness bench_adapter scripts tests
31+
32+
- name: Check dashboard JavaScript
33+
run: node --check aear/static/app.js
34+
35+
- name: Build macOS shell
36+
working-directory: apps/macos
37+
run: |
38+
swift build
39+
swift build -Xswiftc -strict-concurrency=complete
40+
41+
- name: Package unsigned app
42+
run: apps/macos/script/package_unsigned.sh
43+
44+
- name: Release smoke
45+
run: scripts/release_smoke_with_stub.sh

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,8 @@ into memory.
299299

300300
See `docs/native-macos-shell.md` for the SwiftPM package layout, run script,
301301
menu bar behavior, optional hotkey format, and current native-shell limits.
302+
See `docs/macos-signing-notarization.md` for Developer ID signing,
303+
notarization, and release packaging.
302304

303305
## Architecture Notes
304306

apps/macos/script/notarize.sh

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
APP_NAME="hmm"
5+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
6+
DIST_DIR="$ROOT_DIR/dist"
7+
PACKAGE_DIR="$DIST_DIR/package"
8+
APP_BUNDLE="$PACKAGE_DIR/$APP_NAME.app"
9+
SIGNED_ARCHIVE="$DIST_DIR/$APP_NAME-macos-signed.zip"
10+
NOTARIZED_ARCHIVE="$DIST_DIR/$APP_NAME-macos-notarized.zip"
11+
NOTARY_PROFILE="${NOTARY_PROFILE:-hmm-notary}"
12+
NOTARY_TIMEOUT="${NOTARY_TIMEOUT:-30m}"
13+
14+
if [[ ! -f "$SIGNED_ARCHIVE" ]]; then
15+
"$ROOT_DIR/script/package_signed.sh" >/dev/null
16+
fi
17+
18+
if [[ ! -d "$APP_BUNDLE" ]]; then
19+
echo "app bundle not found: $APP_BUNDLE" >&2
20+
exit 1
21+
fi
22+
23+
codesign --verify --strict --verbose=4 "$APP_BUNDLE"
24+
25+
auth_args=()
26+
if [[ -n "${NOTARY_KEY:-}" && -n "${NOTARY_KEY_ID:-}" ]]; then
27+
auth_args+=(--key "$NOTARY_KEY" --key-id "$NOTARY_KEY_ID")
28+
if [[ -n "${NOTARY_ISSUER:-}" ]]; then
29+
auth_args+=(--issuer "$NOTARY_ISSUER")
30+
fi
31+
else
32+
auth_args+=(--keychain-profile "$NOTARY_PROFILE")
33+
fi
34+
35+
xcrun notarytool submit "$SIGNED_ARCHIVE" "${auth_args[@]}" --wait --timeout "$NOTARY_TIMEOUT"
36+
xcrun stapler staple "$APP_BUNDLE"
37+
xcrun stapler validate "$APP_BUNDLE"
38+
spctl -a -vv -t execute "$APP_BUNDLE"
39+
40+
rm -f "$NOTARIZED_ARCHIVE"
41+
(
42+
cd "$PACKAGE_DIR"
43+
ditto -c -k --keepParent "$APP_NAME.app" "$NOTARIZED_ARCHIVE"
44+
)
45+
46+
echo "$NOTARIZED_ARCHIVE"
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
APP_NAME="hmm"
5+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
6+
DIST_DIR="$ROOT_DIR/dist"
7+
PACKAGE_DIR="$DIST_DIR/package"
8+
APP_BUNDLE="$PACKAGE_DIR/$APP_NAME.app"
9+
SIGNED_ARCHIVE="$DIST_DIR/$APP_NAME-macos-signed.zip"
10+
11+
find_default_identity() {
12+
security find-identity -p codesigning -v \
13+
| sed -n 's/.*"\(Developer ID Application: .*\)"/\1/p' \
14+
| head -n 1
15+
}
16+
17+
SIGNING_IDENTITY="${SIGNING_IDENTITY:-$(find_default_identity)}"
18+
if [[ -z "$SIGNING_IDENTITY" ]]; then
19+
cat >&2 <<'EOF'
20+
No Developer ID Application signing identity was found.
21+
22+
Install a Developer ID Application certificate in your login keychain, or set:
23+
SIGNING_IDENTITY="Developer ID Application: Your Name (TEAMID)"
24+
EOF
25+
exit 1
26+
fi
27+
28+
"$ROOT_DIR/script/package_unsigned.sh" >/dev/null
29+
30+
if [[ ! -d "$APP_BUNDLE" ]]; then
31+
echo "app bundle not found: $APP_BUNDLE" >&2
32+
exit 1
33+
fi
34+
35+
sign_args=(--force --timestamp --options runtime --sign "$SIGNING_IDENTITY")
36+
if [[ -n "${HMM_MACOS_ENTITLEMENTS:-}" ]]; then
37+
sign_args+=(--entitlements "$HMM_MACOS_ENTITLEMENTS")
38+
fi
39+
40+
codesign "${sign_args[@]}" "$APP_BUNDLE"
41+
codesign --verify --strict --verbose=4 "$APP_BUNDLE"
42+
43+
rm -f "$SIGNED_ARCHIVE"
44+
(
45+
cd "$PACKAGE_DIR"
46+
ditto -c -k --keepParent "$APP_NAME.app" "$SIGNED_ARCHIVE"
47+
)
48+
49+
echo "$SIGNED_ARCHIVE"

docs/macos-signing-notarization.md

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# macOS Signing and Notarization
2+
3+
This repo can build a local unsigned app without Apple credentials. Public
4+
distribution outside the Mac App Store needs a Developer ID Application
5+
certificate, hardened runtime signing, notarization, and ticket stapling.
6+
7+
## Current Local State
8+
9+
The inspected local artifact is:
10+
11+
```text
12+
apps/macos/dist/hmm.app
13+
```
14+
15+
Current signing state on this machine:
16+
17+
- `codesign -dvvv --entitlements :- apps/macos/dist/hmm.app` reports an ad hoc
18+
signature.
19+
- `security find-identity -p codesigning -v` reports `0 valid identities found`.
20+
- `spctl -a -vv apps/macos/dist/hmm.app` fails before notarization because the
21+
artifact is not Developer ID signed.
22+
23+
This is expected for the unsigned development archive.
24+
25+
## What You Need From Apple
26+
27+
1. Join the Apple Developer Program if the account is not already enrolled.
28+
2. Use the Account Holder role to create a Developer ID Application
29+
certificate. Apple also offers Developer ID Installer certificates, but this
30+
repo ships a zipped `.app`, so the Application certificate is the required
31+
first credential.
32+
3. Install the downloaded `.cer` in Keychain Access. It must appear under
33+
`My Certificates` with its private key.
34+
4. Create notarization credentials:
35+
- Recommended local path: store credentials in Keychain with `notarytool`.
36+
- CI path: use an App Store Connect API key file (`.p8`), key id, and issuer
37+
id as CI secrets. Individual API keys omit issuer id.
38+
39+
## Store Notary Credentials Locally
40+
41+
Use a Keychain profile named `hmm-notary`:
42+
43+
```bash
44+
xcrun notarytool store-credentials hmm-notary \
45+
--apple-id "you@example.com" \
46+
--team-id "TEAMID"
47+
```
48+
49+
`notarytool` will prompt for an app-specific password if you do not pass
50+
`--password`.
51+
52+
For API-key based authentication:
53+
54+
```bash
55+
xcrun notarytool store-credentials hmm-notary \
56+
--key /secure/path/AuthKey_KEYID.p8 \
57+
--key-id KEYID \
58+
--issuer ISSUER-UUID
59+
```
60+
61+
For Individual API Keys, omit `--issuer`.
62+
63+
## Build a Signed Archive
64+
65+
After installing the Developer ID Application certificate:
66+
67+
```bash
68+
apps/macos/script/package_signed.sh
69+
```
70+
71+
The script auto-detects the first `Developer ID Application:` identity. To pin a
72+
specific identity:
73+
74+
```bash
75+
SIGNING_IDENTITY="Developer ID Application: Your Name (TEAMID)" \
76+
apps/macos/script/package_signed.sh
77+
```
78+
79+
Output:
80+
81+
```text
82+
apps/macos/dist/hmm-macos-signed.zip
83+
```
84+
85+
The script signs with:
86+
87+
- `--options runtime`
88+
- `--timestamp`
89+
- Developer ID Application identity
90+
91+
No App Sandbox entitlement is applied. This app launches a local daemon and uses
92+
ScreenCaptureKit/system audio APIs; sandboxing should be reviewed as a separate
93+
product decision, not added casually for Developer ID distribution.
94+
95+
## Notarize and Staple
96+
97+
With a stored Keychain profile:
98+
99+
```bash
100+
apps/macos/script/notarize.sh
101+
```
102+
103+
With direct API key environment variables:
104+
105+
```bash
106+
NOTARY_KEY=/secure/path/AuthKey_KEYID.p8 \
107+
NOTARY_KEY_ID=KEYID \
108+
NOTARY_ISSUER=ISSUER-UUID \
109+
apps/macos/script/notarize.sh
110+
```
111+
112+
For Individual API Keys, leave `NOTARY_ISSUER` unset.
113+
114+
Output:
115+
116+
```text
117+
apps/macos/dist/hmm-macos-notarized.zip
118+
```
119+
120+
The script submits the signed zip with `xcrun notarytool submit --wait`, staples
121+
the ticket to `hmm.app`, validates the staple, runs Gatekeeper assessment with
122+
`spctl`, and creates a final zip from the stapled app.
123+
124+
## Validation Commands
125+
126+
```bash
127+
security find-identity -p codesigning -v
128+
codesign -dvvv --entitlements :- apps/macos/dist/package/hmm.app
129+
codesign --verify --strict --verbose=4 apps/macos/dist/package/hmm.app
130+
xcrun stapler validate apps/macos/dist/package/hmm.app
131+
spctl -a -vv -t execute apps/macos/dist/package/hmm.app
132+
```
133+
134+
## CI Notes
135+
136+
CI should not store a Developer ID private key unless release automation is
137+
explicitly required. The current CI workflow builds and packages unsigned
138+
artifacts only. To add signed CI releases later, store these as repository or
139+
environment secrets:
140+
141+
- `MACOS_CERTIFICATE_P12_BASE64`
142+
- `MACOS_CERTIFICATE_PASSWORD`
143+
- `APPLE_NOTARY_KEY_P8_BASE64`
144+
- `APPLE_NOTARY_KEY_ID`
145+
- `APPLE_NOTARY_ISSUER_ID` for Team API Keys
146+
147+
Create a temporary keychain inside the release job, import the certificate,
148+
run `apps/macos/script/package_signed.sh`, then run
149+
`apps/macos/script/notarize.sh`.
150+
151+
## References
152+
153+
- Apple Developer Program: https://developer.apple.com/programs/
154+
- Developer ID certificates: https://developer.apple.com/help/account/certificates/create-developer-id-certificates/
155+
- Create a private key: https://developer.apple.com/help/account/keys/create-a-private-key
156+
- Notarizing macOS software: https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution
157+
- Customizing notarization workflow: https://developer.apple.com/documentation/security/customizing-the-notarization-workflow

docs/release-readiness.md

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,6 @@ unsigned local archive. It does not claim the app is signed or notarized.
66

77
## Local Release Check
88

9-
Start the stub daemon in one terminal:
10-
11-
```bash
12-
uv run hmm --profile stub --host 127.0.0.1 --port 8765
13-
```
14-
159
Run the full local release check from the repository root:
1610

1711
```bash
@@ -25,11 +19,13 @@ This runs:
2519
- Dashboard JavaScript syntax check when `node` is available.
2620
- Native macOS build-and-launch verification.
2721
- Release-mode unsigned app packaging.
28-
- `scripts/release_smoke.py` against the running daemon and local app archive.
22+
- `scripts/release_smoke_with_stub.sh`, which starts a stub daemon, waits for
23+
`/health`, runs `scripts/release_smoke.py`, and shuts the daemon down.
2924

3025
For a read-only smoke check after packaging:
3126

3227
```bash
28+
uv run hmm --profile stub --host 127.0.0.1 --port 8765
3329
scripts/release_smoke.py --server http://127.0.0.1:8765
3430
```
3531

@@ -70,12 +66,13 @@ regression testing, but it is not a signed public distribution artifact.
7066

7167
For signed distribution, the next packaging work must add:
7268

73-
- A Developer ID Application signing identity.
74-
- Hardened runtime settings.
75-
- Entitlement review for ScreenCaptureKit, automation, and launch-at-login use.
76-
- Signing of the app bundle and any nested helper code.
77-
- Notarization with `notarytool`.
78-
- Stapling and Gatekeeper verification with `spctl`.
69+
- A Developer ID Application signing identity installed in Keychain.
70+
- Notary credentials stored through `notarytool` or passed as API-key
71+
environment variables.
72+
- `apps/macos/script/package_signed.sh`.
73+
- `apps/macos/script/notarize.sh`.
74+
75+
See `docs/macos-signing-notarization.md` for the exact setup and commands.
7976

8077
## Rename And Migration Notes
8178

scripts/release_smoke_with_stub.sh

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
HOST="${HMM_HOST:-127.0.0.1}"
5+
PORT="${HMM_PORT:-8765}"
6+
SERVER="http://$HOST:$PORT"
7+
LOG_FILE="${HMM_SMOKE_LOG:-/tmp/hmm-release-smoke.log}"
8+
export UV_CACHE_DIR="${UV_CACHE_DIR:-.uv-cache}"
9+
10+
cleanup() {
11+
if [[ -n "${SERVER_PID:-}" ]] && ps -p "$SERVER_PID" >/dev/null 2>&1; then
12+
kill "$SERVER_PID" >/dev/null 2>&1 || true
13+
wait "$SERVER_PID" >/dev/null 2>&1 || true
14+
fi
15+
}
16+
trap cleanup EXIT
17+
18+
uv run hmm --profile stub --host "$HOST" --port "$PORT" >"$LOG_FILE" 2>&1 &
19+
SERVER_PID="$!"
20+
21+
uv run python - "$SERVER/health" <<'PY'
22+
from __future__ import annotations
23+
24+
import sys
25+
import time
26+
from urllib.error import URLError
27+
from urllib.request import urlopen
28+
29+
url = sys.argv[1]
30+
deadline = time.monotonic() + 20
31+
while time.monotonic() < deadline:
32+
try:
33+
with urlopen(url, timeout=1) as response:
34+
if response.status == 200:
35+
raise SystemExit(0)
36+
except URLError:
37+
time.sleep(0.25)
38+
raise SystemExit(f"daemon did not become healthy: {url}")
39+
PY
40+
41+
scripts/release_smoke.py --server "$SERVER"

0 commit comments

Comments
 (0)