Skip to content

Commit 8f90700

Browse files
committed
signing and notarizing
1 parent caa5ab8 commit 8f90700

8 files changed

Lines changed: 262 additions & 12 deletions

File tree

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# Build, sign, and notarize the macOS app. Run manually or on tag.
2+
# Required secrets: APPLE_CERTIFICATE, APPLE_CERTIFICATE_PASSWORD, APPLE_ID,
3+
# APPLE_APP_SPECIFIC_PASSWORD, KEYCHAIN_PASSWORD
4+
# Optional: APPLE_TEAM_ID (defaults to DP9DFGMC65 in notarize script)
5+
6+
name: Release macOS
7+
8+
on:
9+
workflow_dispatch:
10+
push:
11+
tags:
12+
- '*'
13+
14+
jobs:
15+
release-macos:
16+
runs-on: macos-latest
17+
steps:
18+
- name: Checkout repository
19+
uses: actions/checkout@v4
20+
21+
- name: Set version from tag
22+
id: version
23+
run: |
24+
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
25+
echo "version=${{ github.ref_name }}" >> $GITHUB_OUTPUT
26+
else
27+
echo "version=0.0.0" >> $GITHUB_OUTPUT
28+
fi
29+
30+
- name: Write version file and update app config
31+
run: |
32+
VERSION="${{ steps.version.outputs.version }}"
33+
echo "$VERSION" > app/version.txt
34+
sed -i.bak "s/\"version\": \"[^\"]*\"/\"version\": \"$VERSION\"/" app/package.json && rm -f app/package.json.bak
35+
sed -i.bak "s/\"version\":\"[^\"]*\"/\"version\":\"$VERSION\"/" app/src-tauri/tauri.conf.json && rm -f app/src-tauri/tauri.conf.json.bak
36+
sed -i.bak "s/^version = \"[^\"]*\"/version = \"$VERSION\"/" app/src-tauri/Cargo.toml && rm -f app/src-tauri/Cargo.toml.bak
37+
38+
- name: Setup Node.js
39+
uses: actions/setup-node@v4
40+
with:
41+
node-version: 20
42+
cache: 'npm'
43+
cache-dependency-path: app/package-lock.json
44+
45+
- name: Install Rust
46+
uses: dtolnay/rust-toolchain@stable
47+
48+
- name: Import Apple Developer certificate
49+
env:
50+
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
51+
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
52+
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
53+
run: |
54+
echo "$APPLE_CERTIFICATE" | base64 --decode > certificate.p12
55+
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
56+
security default-keychain -s build.keychain
57+
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
58+
security set-keychain-settings -t 3600 -u build.keychain
59+
security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
60+
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
61+
rm certificate.p12
62+
63+
- name: Get signing identity
64+
id: identity
65+
run: |
66+
SIGNING_IDENTITY=$(security find-identity -v -p codesigning build.keychain | grep "Developer ID Application" | head -1 | sed -n 's/.*"\(.*\)".*/\1/p')
67+
if [ -z "$SIGNING_IDENTITY" ]; then
68+
echo "No Developer ID Application identity found in keychain"
69+
security find-identity -v -p codesigning build.keychain
70+
exit 1
71+
fi
72+
echo "identity=$SIGNING_IDENTITY" >> $GITHUB_OUTPUT
73+
echo "SIGNING_IDENTITY=$SIGNING_IDENTITY" >> $GITHUB_ENV
74+
75+
- name: Install dependencies
76+
working-directory: app
77+
run: npm ci
78+
79+
- name: Build and sign (Tauri + re-sign with timestamp + hardened runtime)
80+
working-directory: app
81+
env:
82+
SIGNING_IDENTITY: ${{ steps.identity.outputs.identity }}
83+
run: npm run tauri:build:release
84+
85+
- name: Notarize
86+
working-directory: app
87+
env:
88+
APPLE_ID: ${{ secrets.APPLE_ID }}
89+
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
90+
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
91+
run: npm run notarize
92+
93+
- name: Upload notarized app
94+
uses: actions/upload-artifact@v4
95+
with:
96+
name: macos-notarized-app-${{ steps.version.outputs.version }}
97+
path: |
98+
app/src-tauri/target/release/bundle/macos/neopixel-blocks.app
99+
app/version.txt

app/package-lock.json

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,16 @@
1010
"lint": "eslint .",
1111
"preview": "vite preview",
1212
"serve": "npm run build && npx http-serve dist -p 80",
13+
"tauri": "tauri",
1314
"tauri:dev": "npx tauri dev",
1415
"tauri:build": "npx tauri build",
16+
"tauri:build:release": "npm run tauri:build && sh scripts/resign-macos.sh",
1517
"tauri:android:init": "npx tauri android init",
1618
"tauri:android:build": "tauri android build",
17-
"tauri:ios:init": "npx tauri ios init",
18-
"tauri:ios:build": "npx tauri ios build"
19+
"tauri:ios:check": "node -e \"try{require('child_process').execSync('which pod',{stdio:'pipe'});}catch(e){console.error('\\nCocoaPods (pod) is required for Tauri iOS. Install it first:\\n brew install cocoapods\\nor\\n sudo gem install cocoapods\\n');process.exit(1);}\"",
20+
"tauri:ios:init": "npm run tauri:ios:check && npx tauri ios init",
21+
"tauri:ios:build": "npx tauri ios build",
22+
"notarize": "sh scripts/notarize.sh"
1923
},
2024
"dependencies": {
2125
"@emotion/react": "^11.14.0",

app/scripts/notarize.sh

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#!/usr/bin/env bash
2+
set -e
3+
4+
# Notarize the built macOS app. Run after: npm run tauri:build:release
5+
# (tauri:build:release re-signs with timestamp + hardened runtime and removes the DMG)
6+
# Requires env: APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD
7+
# Optional: APPLE_TEAM_ID (defaults to DP9DFGMC65)
8+
9+
APPLE_TEAM_ID="${APPLE_TEAM_ID:-DP9DFGMC65}"
10+
BUNDLE="src-tauri/target/release/bundle"
11+
APP="$BUNDLE/macos/neopixel-blocks.app"
12+
DMG=$(echo $BUNDLE/dmg/*.dmg 2>/dev/null || true)
13+
14+
if [ -z "$APPLE_ID" ] || [ -z "$APPLE_APP_SPECIFIC_PASSWORD" ]; then
15+
echo "Set APPLE_ID and APPLE_APP_SPECIFIC_PASSWORD (app-specific password from appleid.apple.com)"
16+
exit 1
17+
fi
18+
19+
if [ ! -d "$BUNDLE" ]; then
20+
echo "Bundle not found at $BUNDLE. Run: npm run tauri:build:release"
21+
exit 1
22+
fi
23+
24+
# Only notarize the .app after tauri:build:release (re-signed; DMG is removed then).
25+
# If DMG still exists, the .app was not re-signed — Apple will reject it.
26+
if [ -f "$DMG" ]; then
27+
echo "DMG still present. Run: npm run tauri:build:release"
28+
echo "That re-signs the app with timestamp + hardened runtime and removes the DMG. Then run npm run notarize again."
29+
exit 1
30+
fi
31+
32+
if [ -d "$APP" ]; then
33+
ZIP="$BUNDLE/macos/neopixel-blocks.zip"
34+
echo "Submitting .app for notarization (zipping)..."
35+
ditto -c -k --keepParent "$APP" "$ZIP"
36+
SUBMIT_OUTPUT=$(xcrun notarytool submit "$ZIP" \
37+
--apple-id "$APPLE_ID" \
38+
--password "$APPLE_APP_SPECIFIC_PASSWORD" \
39+
--team-id "$APPLE_TEAM_ID" \
40+
--wait 2>&1) || true
41+
rm -f "$ZIP"
42+
echo "$SUBMIT_OUTPUT"
43+
if echo "$SUBMIT_OUTPUT" | grep -q "status: Invalid"; then
44+
SUBMISSION_ID=$(echo "$SUBMIT_OUTPUT" | grep "id:" | head -1 | sed 's/.*id: *//' | tr -d ' ')
45+
echo "Notarization failed. Fetching Apple's rejection reason..."
46+
if [ -n "$SUBMISSION_ID" ]; then
47+
xcrun notarytool log "$SUBMISSION_ID" \
48+
--apple-id "$APPLE_ID" \
49+
--password "$APPLE_APP_SPECIFIC_PASSWORD" \
50+
--team-id "$APPLE_TEAM_ID" 2>&1 || true
51+
fi
52+
exit 1
53+
fi
54+
if ! echo "$SUBMIT_OUTPUT" | grep -q "status: Accepted"; then
55+
echo "Unexpected notarization status. Aborting."
56+
exit 1
57+
fi
58+
echo "Stapling notarization ticket to app..."
59+
xcrun stapler staple "$APP"
60+
echo "Done. Notarized: $APP"
61+
else
62+
echo "No .app found under $BUNDLE. Run: npm run tauri:build:release"
63+
exit 1
64+
fi

app/scripts/resign-macos.sh

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
#!/usr/bin/env bash
2+
# Re-sign the macOS app binary with secure timestamp and hardened runtime
3+
# so it passes Apple notarization. Run after: npm run tauri:build
4+
# Set SIGNING_IDENTITY to your keychain identity, e.g.:
5+
# security find-identity -v -p codesigning
6+
# export SIGNING_IDENTITY="Developer ID Application: Your Name (TEAM_ID)"
7+
8+
set -e
9+
if [ -z "$SIGNING_IDENTITY" ]; then
10+
echo "SIGNING_IDENTITY not set. Find your identity with: security find-identity -v -p codesigning"
11+
echo "Then run: export SIGNING_IDENTITY=\"Developer ID Application: Your Name (TEAM_ID)\""
12+
exit 1
13+
fi
14+
BUNDLE="src-tauri/target/release/bundle"
15+
APP="$BUNDLE/macos/neopixel-blocks.app"
16+
BINARY="$APP/Contents/MacOS/app"
17+
ENTITLEMENTS="src-tauri/Entitlements.plist"
18+
19+
if [ ! -f "$BINARY" ]; then
20+
echo "Binary not found at $BINARY. Run: npm run tauri:build"
21+
exit 1
22+
fi
23+
24+
if ! security find-identity -v -p codesigning | grep -Fq "$SIGNING_IDENTITY"; then
25+
echo "Signing identity not found in keychain: $SIGNING_IDENTITY"
26+
echo ""
27+
echo "List available identities:"
28+
security find-identity -v -p codesigning
29+
echo ""
30+
echo "Use the exact string in quotes (e.g. \"Developer ID Application: ...\") and run:"
31+
echo " export SIGNING_IDENTITY=\"<your exact identity>\""
32+
echo " npm run tauri:build:release"
33+
exit 1
34+
fi
35+
36+
echo "Re-signing main binary with timestamp and hardened runtime..."
37+
if ! codesign --force --timestamp --options runtime \
38+
-s "$SIGNING_IDENTITY" \
39+
--entitlements "$ENTITLEMENTS" \
40+
"$BINARY"; then
41+
echo ""
42+
echo "Resign failed. DMG was not removed. Fix SIGNING_IDENTITY/keychain and run: npm run tauri:build:release"
43+
exit 1
44+
fi
45+
46+
echo "Re-signing app bundle..."
47+
if ! codesign --force --timestamp --options runtime \
48+
-s "$SIGNING_IDENTITY" \
49+
"$APP"; then
50+
echo ""
51+
echo "Resign failed. DMG was not removed. Fix SIGNING_IDENTITY/keychain and run: npm run tauri:build:release"
52+
exit 1
53+
fi
54+
55+
# Remove DMG so notarize uses the re-signed .app (DMG would still contain old copy)
56+
rm -f "$BUNDLE/dmg/"*.dmg 2>/dev/null || true
57+
58+
echo "Done. Run npm run notarize to submit the re-signed .app."

app/src-tauri/.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
# Generated by Cargo
22
# will have compiled files and executables
33
/target/
4+
5+
# Generated platform projects (Xcode, Android, etc.)
6+
/gen/
7+
8+
# Generated schemas (if any)
49
/gen/schemas

app/src-tauri/Entitlements.plist

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>com.apple.security.cs.allow-jit</key>
6+
<true/>
7+
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
8+
<true/>
9+
<key>com.apple.security.cs.disable-library-validation</key>
10+
<true/>
11+
</dict>
12+
</plist>

app/src-tauri/tauri.conf.json

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@
3232
"icons/128x128@2x.png",
3333
"icons/icon.icns",
3434
"icons/icon.ico"
35-
]
35+
],
36+
"macOS": {
37+
"signingIdentity": null,
38+
"hardenedRuntime": true,
39+
"entitlements": "./Entitlements.plist"
40+
},
41+
"iOS": {
42+
"developmentTeam": "YOUR_APPLE_TEAM_ID"
43+
}
3644
}
37-
}
45+
}

0 commit comments

Comments
 (0)