LivelyKernel / LivelyKernel/lively.next
macOS: code-sign and notarize lively.next.app so downloads open on double-click
- Dominant language
- JavaScript
- Stars
- 90
- Forks
- 20
- PR merge metrics
- No merged PRs in 30d
Description
## Background
The nightly CI (`.github/workflows/build-desktop-app.yml`) produces `lively.next-osx-arm64.tar.gz` from a `macos-latest` runner. When a user downloads it and double-clicks the contained `lively.next.app`, macOS shows:
> "lively.next.app is damaged and can't be opened. You should move it to the Trash."
The app isn't damaged — Gatekeeper refuses to launch it because:
1. The `.app` is **unsigned** (no Apple Developer ID signature on any of its binaries).
2. The `.app` is **not notarized** (not uploaded to Apple's notary service for malware scan + ticket stapling).
3. The download carries a `com.apple.quarantine` extended attribute set by the browser, which tells Gatekeeper to block an unsigned/unnotarized app by default on macOS 10.15+.
For now the bundle ships a `README-macOS.txt` explaining the `xattr -cr lively.next.app` workaround, but that's a poor out-of-box experience. This issue tracks the proper fix.
## What needs to happen
End state: a user downloads the bundle, double-clicks `lively.next.app`, and it just runs — no terminal gymnastics, no Privacy & Security override.
Two steps in order:
### 1. Code-signing
Every Mach-O binary inside the `.app` needs to be signed with an Apple Developer ID certificate. That includes:
- `Contents/MacOS/nwjs` (the main NW.js binary)
- `Contents/Frameworks/nwjs Framework.framework/Versions/*/nwjs Framework` (the helper app and its inner framework)
- `Contents/Frameworks/nwjs Framework.framework/Versions/*/Helpers/nwjs Helper*.app/Contents/MacOS/*`
- `Contents/Resources/app.nw/node/bin/node` (our bundled Node.js binary)
- Any `.dylib` in `Contents/Frameworks/` / `Contents/Resources/`
Sign from inner-to-outer using `codesign --deep --force --options runtime --sign 'Developer ID Application: ()'`, then sign the outer `.app` last. `--options runtime` enables the Hardened Runtime, which is required for notarization.
### 2. Notarization
Once signed, zip the `.app` and submit to Apple's notary service:
```
xcrun notarytool submit lively.next.app.zip --apple-id ... --team-id ... --password ... --wait
xcrun stapler staple lively.next.app
```
Stapling embeds the notarization ticket into the `.app` so Gatekeeper can verify it offline.
## Requirements
- **Apple Developer Program membership** (US\$99/year) — needed to issue a Developer ID Application certificate.
- **Developer ID certificate** exported as a `.p12` with a password. Store as a GitHub Actions secret (base64-encoded) plus the password as a separate secret.
- **App-specific password** for the Apple ID used by `notarytool`, stored as a secret.
- A **Team ID** (found in the Developer portal).
## CI plan
Add these steps to `build-desktop-app.yml`, inside the `macos-latest` matrix entry, between "Build desktop bundle" and "Upload bundle artifact":
```yaml
- name: Import code-signing certificate
if: runner.os == 'macOS' && env.APPLE_SIGNING_CERT != ''
env:
APPLE_SIGNING_CERT: ${{ secrets.APPLE_SIGNING_CERT_P12_BASE64 }}
APPLE_SIGNING_CERT_PASSWORD: ${{ secrets.APPLE_SIGNING_CERT_PASSWORD }}
run: |
KEYCHAIN=build.keychain
echo "$APPLE_SIGNING_CERT" | base64 --decode > /tmp/cert.p12
security create-keychain -p "" $KEYCHAIN
security default-keychain -s $KEYCHAIN
security unlock-keychain -p "" $KEYCHAIN
security import /tmp/cert.p12 -k $KEYCHAIN -P "$APPLE_SIGNING_CERT_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "" $KEYCHAIN
rm /tmp/cert.p12
- name: Code-sign the .app
if: runner.os == 'macOS' && env.APPLE_SIGNING_CERT != ''
run: |
APP=dist/lively.next-osx-arm64/lively.next.app
# Sign inner binaries first, outer last
find "$APP/Contents" -type f \( -name '*.dylib' -o -perm +111 \) \
-exec codesign --force --options runtime \
--sign "$SIGNING_IDENTITY" {} \;
codesign --force --deep --options runtime \
--sign "$SIGNING_IDENTITY" "$APP"
- name: Notarize + staple
if: runner.os == 'macOS' && env.APPLE_ID != ''
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
run: |
APP=dist/lively.next-osx-arm64/lively.next.app
ditto -c -k --keepParent "$APP" /tmp/lively.next.app.zip
xcrun notarytool submit /tmp/lively.next.app.zip \
--apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID" \
--password "$APPLE_APP_SPECIFIC_PASSWORD" --wait
xcrun stapler staple "$APP"
```
The `if: env.* != ''` guards make signing a no-op when the secrets aren't set (for forks / dry-runs), so the build still produces an unsigned `.app` that power users can strip quarantine off of manually.
## Remaining work after signing
- Drop `README-macOS.txt` from the bundle (no longer needed).
- Update `lively.app/scripts/build.mjs`'s `finalizeMacOS` to skip the README when `SIGNING_IDENTITY` env is set (or just always drop it once signing lands).
## Relevant docs
- Apple: [Notarizing macOS software before distribution](https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution)
- Apple: [Customizing the notarization workflow](https://developer.apple.com/documentation/security/customizing_the_notarization_workflow)
- `codesign(1)` man page, `notarytool(1)` man page
## Out of scope here
- macOS x64 builds (Intel) — the matrix only produces osx-arm64 right now; Intel support is a separate follow-up if there's demand.
- Gatekeeper on Windows (SmartScreen) — separate issue; Windows bundles face an analogous problem with EV code-signing certificates.
Contributor guide
Research direction
Start with .github/workflows/build-desktop-app.yml and lively.app/scripts/build.mjs, then inspect the macOS artifact produced by the existing desktop build. Verify the certificate and notarization secrets can be used safely in the macOS job, and confirm the resulting lively.next.app passes codesign and notarization checks and opens by double-click without the README workaround.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- github-actions, javascript, macos
- Domain
- ci-cd, release, security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 35/100