Skip to content

Commit 08c3c87

Browse files
committed
fix: require signed macos release bundles
1 parent 6478177 commit 08c3c87

4 files changed

Lines changed: 175 additions & 0 deletions

File tree

.github/workflows/macos.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,40 @@ jobs:
7474
echo "value=$PROFILE" >> "$GITHUB_OUTPUT"
7575
if [ "$PROFILE" = "fast" ]; then echo "target_dir=fast" >> "$GITHUB_OUTPUT"; else echo "target_dir=release" >> "$GITHUB_OUTPUT"; fi
7676
77+
- name: Configure macOS signing + notarization
78+
env:
79+
REQUIRE_MACOS_SIGNING: ${{ startsWith(github.ref, 'refs/tags/') }}
80+
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
81+
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
82+
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
83+
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
84+
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
85+
APPLE_API_KEY_BASE64: ${{ secrets.APPLE_API_KEY_BASE64 }}
86+
APPLE_ID: ${{ secrets.APPLE_ID }}
87+
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
88+
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
89+
run: |
90+
bash scripts/release/setup-macos-signing.sh
91+
7792
- name: Build macOS app (Tauri)
93+
env:
94+
APPLE_SIGNING_IDENTITY: ${{ env.APPLE_SIGNING_IDENTITY }}
95+
APPLE_API_KEY: ${{ env.APPLE_API_KEY }}
96+
APPLE_API_ISSUER: ${{ env.APPLE_API_ISSUER }}
97+
APPLE_API_KEY_PATH: ${{ env.APPLE_API_KEY_PATH }}
98+
APPLE_ID: ${{ env.APPLE_ID }}
99+
APPLE_PASSWORD: ${{ env.APPLE_PASSWORD }}
100+
APPLE_TEAM_ID: ${{ env.APPLE_TEAM_ID }}
78101
run: |
79102
node scripts/tauri/run-tauri-build.js --profile ${{ steps.build_profile.outputs.value }}
80103
104+
- name: Verify signed + notarized macOS bundle
105+
if: startsWith(github.ref, 'refs/tags/')
106+
run: |
107+
app_path="$(find "src-tauri/target/${{ steps.build_profile.outputs.target_dir }}/bundle/macos" -maxdepth 1 -type d -name '*.app' | head -n 1)"
108+
dmg_path="$(find "src-tauri/target/${{ steps.build_profile.outputs.target_dir }}/bundle/dmg" -maxdepth 1 -type f -name '*.dmg' | head -n 1)"
109+
bash scripts/release/verify-macos-bundle.sh "$app_path" "$dmg_path"
110+
81111
- name: Upload build artifacts
82112
uses: actions/upload-artifact@v4
83113
with:

CODEBASE_DOCUMENTATION.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,8 @@ scripts/release/check-version-consistency.js - Release metadata guardrail
133133
scripts/release/verify-bundle-version.js - Bundle filename verifier
134134
├─ Validates: Windows `.exe`/`.msi` and macOS `.dmg` filenames include the expected release version
135135
└─ Failure mode: catches stale cached artifacts that wildcard GitHub release uploads would otherwise attach
136+
scripts/release/setup-macos-signing.sh - GitHub Actions helper that imports the Developer ID certificate into a temp keychain, resolves the signing identity, and exports notarization credentials for Tauri builds
137+
scripts/release/verify-macos-bundle.sh - GitHub Actions helper that verifies macOS release bundles with `codesign`, `spctl`, and `xcrun stapler validate` before tag uploads
136138
scripts/debug/ - Manual debug helpers kept out of the repo root
137139
├─ `test-button-merge.js` verifies config merge behavior against `WorkspaceManager`
138140
└─ `test-cascade-debug.js` prints cascaded config layers for manual inspection
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
require_signing="${REQUIRE_MACOS_SIGNING:-false}"
5+
require_signing="$(printf '%s' "$require_signing" | tr '[:upper:]' '[:lower:]')"
6+
7+
decode_base64_to_file() {
8+
local encoded="$1"
9+
local output_path="$2"
10+
11+
if printf '%s' "$encoded" | base64 --decode > "$output_path" 2>/dev/null; then
12+
return 0
13+
fi
14+
15+
printf '%s' "$encoded" | base64 -D > "$output_path"
16+
}
17+
18+
has_cert=false
19+
if [[ -n "${APPLE_CERTIFICATE:-}" && -n "${APPLE_CERTIFICATE_PASSWORD:-}" ]]; then
20+
has_cert=true
21+
fi
22+
23+
has_notary_api=false
24+
if [[ -n "${APPLE_API_KEY:-}" && -n "${APPLE_API_ISSUER:-}" && -n "${APPLE_API_KEY_BASE64:-}" ]]; then
25+
has_notary_api=true
26+
fi
27+
28+
has_notary_apple_id=false
29+
if [[ -n "${APPLE_ID:-}" && -n "${APPLE_PASSWORD:-}" && -n "${APPLE_TEAM_ID:-}" ]]; then
30+
has_notary_apple_id=true
31+
fi
32+
33+
if [[ "$has_cert" != true ]]; then
34+
if [[ "$require_signing" == "true" ]]; then
35+
echo "::error::Tag releases require APPLE_CERTIFICATE and APPLE_CERTIFICATE_PASSWORD secrets."
36+
exit 1
37+
fi
38+
echo "::notice::Skipping macOS signing setup because certificate secrets are not configured."
39+
exit 0
40+
fi
41+
42+
if [[ "$has_notary_api" != true && "$has_notary_apple_id" != true ]]; then
43+
if [[ "$require_signing" == "true" ]]; then
44+
echo "::error::Tag releases require notarization credentials. Set either APPLE_API_KEY + APPLE_API_ISSUER + APPLE_API_KEY_BASE64 or APPLE_ID + APPLE_PASSWORD + APPLE_TEAM_ID."
45+
exit 1
46+
fi
47+
echo "::notice::Skipping notarization configuration because notarization secrets are not configured."
48+
fi
49+
50+
if [[ -z "${RUNNER_TEMP:-}" || -z "${GITHUB_ENV:-}" ]]; then
51+
echo "::error::RUNNER_TEMP and GITHUB_ENV must be available in GitHub Actions."
52+
exit 1
53+
fi
54+
55+
cert_path="$RUNNER_TEMP/agent-workspace-macos-signing.p12"
56+
keychain_path="$RUNNER_TEMP/agent-workspace-signing.keychain-db"
57+
keychain_password="${KEYCHAIN_PASSWORD:-agent-workspace-$(date +%s)-$$}"
58+
59+
decode_base64_to_file "$APPLE_CERTIFICATE" "$cert_path"
60+
61+
security create-keychain -p "$keychain_password" "$keychain_path"
62+
security set-keychain-settings -lut 21600 "$keychain_path"
63+
security unlock-keychain -p "$keychain_password" "$keychain_path"
64+
security import "$cert_path" \
65+
-k "$keychain_path" \
66+
-P "$APPLE_CERTIFICATE_PASSWORD" \
67+
-T /usr/bin/codesign \
68+
-T /usr/bin/security \
69+
-T /usr/bin/productbuild
70+
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$keychain_password" "$keychain_path"
71+
security list-keychains -d user -s "$keychain_path"
72+
security default-keychain -d user -s "$keychain_path"
73+
74+
signing_identity="${APPLE_SIGNING_IDENTITY:-}"
75+
if [[ -z "$signing_identity" ]]; then
76+
signing_identity="$(security find-identity -v -p codesigning "$keychain_path" | awk -F'"' '/Developer ID Application/ { print $2; exit }')"
77+
fi
78+
if [[ -z "$signing_identity" ]]; then
79+
signing_identity="$(security find-identity -v -p codesigning "$keychain_path" | awk -F'"' 'NR == 1 { print $2 }')"
80+
fi
81+
if [[ -z "$signing_identity" ]]; then
82+
echo "::error::Unable to resolve a macOS code-signing identity from the imported certificate."
83+
exit 1
84+
fi
85+
86+
{
87+
echo "APPLE_SIGNING_IDENTITY=$signing_identity"
88+
echo "MACOS_SIGNING_KEYCHAIN=$keychain_path"
89+
} >> "$GITHUB_ENV"
90+
91+
if [[ "$has_notary_api" == true ]]; then
92+
api_key_path="$RUNNER_TEMP/AuthKey_${APPLE_API_KEY}.p8"
93+
decode_base64_to_file "$APPLE_API_KEY_BASE64" "$api_key_path"
94+
{
95+
echo "APPLE_API_KEY_PATH=$api_key_path"
96+
echo "APPLE_API_KEY=$APPLE_API_KEY"
97+
echo "APPLE_API_ISSUER=$APPLE_API_ISSUER"
98+
} >> "$GITHUB_ENV"
99+
fi
100+
101+
if [[ "$has_notary_apple_id" == true ]]; then
102+
{
103+
echo "APPLE_ID=$APPLE_ID"
104+
echo "APPLE_PASSWORD=$APPLE_PASSWORD"
105+
echo "APPLE_TEAM_ID=$APPLE_TEAM_ID"
106+
} >> "$GITHUB_ENV"
107+
fi
108+
109+
echo "::notice::Configured macOS signing identity: $signing_identity"
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
if [[ "$#" -lt 1 ]]; then
5+
echo "usage: $0 <app-path> [dmg-path]"
6+
exit 1
7+
fi
8+
9+
app_path="$1"
10+
dmg_path="${2:-}"
11+
12+
if [[ ! -d "$app_path" ]]; then
13+
echo "::error::macOS app bundle not found: $app_path"
14+
exit 1
15+
fi
16+
17+
echo "[release] Verifying codesign for $app_path"
18+
codesign --verify --deep --strict --verbose=2 "$app_path"
19+
codesign -dv --verbose=4 "$app_path"
20+
21+
echo "[release] Assessing Gatekeeper policy for $app_path"
22+
spctl --assess --type execute --verbose=4 "$app_path"
23+
24+
echo "[release] Validating notarization staple for $app_path"
25+
xcrun stapler validate "$app_path"
26+
27+
if [[ -n "$dmg_path" ]]; then
28+
if [[ ! -f "$dmg_path" ]]; then
29+
echo "::error::macOS DMG not found: $dmg_path"
30+
exit 1
31+
fi
32+
echo "[release] Validating notarization staple for $dmg_path"
33+
xcrun stapler validate "$dmg_path"
34+
fi

0 commit comments

Comments
 (0)