Ozgur Ersoy 574a17e357
All checks were successful
Test macOS Build Action / test-macos-build (push) Successful in 46m3s
fix(actions): enhance macOS notarization script with verification steps and DMG package creation
2025-04-14 23:43:42 +02:00

717 lines
32 KiB
YAML

name: "macOS Sign and Notarize"
description: "Signs and notarizes macOS applications with Developer ID certificate"
author: moersoy"
inputs:
app-path:
description: "Path to the app bundle (.app)"
required: true
entitlements-file:
description: "Path to the entitlements file (.entitlements)"
required: true
team-id:
description: "Apple Developer Team ID"
required: true
certificate-base64:
description: "Base64-encoded Developer ID Application certificate (.p12)"
required: true
certificate-password:
description: "Password for the Developer ID Application certificate"
required: true
notarization-method:
description: "Method to use for notarization: 'api-key' or 'app-password'"
required: false
default: "api-key"
notary-user:
description: "Apple ID for notarization (for app-password method)"
required: false
notary-password:
description: "App-specific password for notarization (for app-password method)"
required: false
notary-api-key-id:
description: "API Key ID for notarization (for api-key method)"
required: false
notary-api-key-issuer-id:
description: "API Issuer ID for notarization (for api-key method)"
required: false
notary-api-key-path:
description: "Path to or content of the API Key .p8 file (for api-key method)"
required: false
bundle-id:
description: "App bundle identifier (com.example.app)"
required: false
fallback-to-adhoc:
description: "Whether to fall back to ad-hoc signing if certificate is invalid"
required: false
default: "true"
outputs:
signed:
description: "Whether the app was signed (identity, adhoc, or none)"
value: ${{ steps.sign.outputs.signed }}
notarized:
description: "Whether the app was notarized (true or false)"
value: ${{ steps.notarize.outputs.notarized }}
package-path:
description: "Path to the final package"
value: ${{ steps.package.outputs.package-path }}
runs:
using: "composite"
steps:
- name: Setup Certificate
id: setup-cert
shell: bash
env:
CERTIFICATE_BASE64: ${{ inputs.certificate-base64 }}
CERTIFICATE_PASSWORD: ${{ inputs.certificate-password }}
APPLE_TEAM_ID: ${{ inputs.team-id }}
run: |
echo "🔐 Setting up certificate..."
# Create a temporary directory for certificates
CERT_DIR="$HOME/certificates"
mkdir -p "$CERT_DIR"
# Decode the certificate to a p12 file
echo "$CERTIFICATE_BASE64" | base64 --decode > "$CERT_DIR/certificate.p12"
# Create keychain
KEYCHAIN_PATH="$CERT_DIR/app-signing.keychain-db"
KEYCHAIN_PASSWORD="temppassword123"
# Delete existing keychain if it exists
security delete-keychain "$KEYCHAIN_PATH" 2>/dev/null || true
# Create new keychain
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -t 3600 -u -l "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
# Add to search list and make default
security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | tr -d '"')
security default-keychain -s "$KEYCHAIN_PATH"
# Import certificate
echo "🔑 Importing developer certificate..."
security import "$CERT_DIR/certificate.p12" -k "$KEYCHAIN_PATH" -P "$CERTIFICATE_PASSWORD" -T /usr/bin/codesign
# Try with additional parameters if needed
security import "$CERT_DIR/certificate.p12" -k "$KEYCHAIN_PATH" -P "$CERTIFICATE_PASSWORD" -T /usr/bin/codesign -f pkcs12 || true
# Set partition list for codesign to access keychain
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
# Verify certificate
echo "🔍 Verifying code signing identities..."
security find-identity -v -p codesigning "$KEYCHAIN_PATH"
# Try to use the System keychain as a fallback
echo "🔍 Checking system keychain for code signing identities..."
SYSTEM_IDENTITIES=$(security find-identity -v -p codesigning)
echo "$SYSTEM_IDENTITIES"
if echo "$SYSTEM_IDENTITIES" | grep -q "Developer ID Application"; then
echo "✅ Found Developer ID Application certificate in system keychain"
echo "::set-output name=use_system_cert::true"
else
echo "::set-output name=use_system_cert::false"
fi
# Store keychain variables for later steps
echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> $GITHUB_ENV
echo "KEYCHAIN_PASSWORD=$KEYCHAIN_PASSWORD" >> $GITHUB_ENV
echo "APPLE_TEAM_ID=$APPLE_TEAM_ID" >> $GITHUB_ENV
# Clean up
rm -f "$CERT_DIR/certificate.p12"
- name: Sign App
id: sign
shell: bash
run: |
echo "🔏 Signing app with Developer ID certificate..."
# Check if app path exists
if [ ! -d "${{ inputs.app-path }}" ]; then
echo "❌ App bundle not found at ${{ inputs.app-path }}"
echo "::set-output name=signed::none"
exit 1
fi
# Check if entitlements file exists
if [ ! -f "${{ inputs.entitlements-file }}" ]; then
echo "❌ Entitlements file not found at ${{ inputs.entitlements-file }}"
echo "::set-output name=signed::none"
exit 1
fi
# Decide which keychain to use
if [ "${{ steps.setup-cert.outputs.use_system_cert }}" = "true" ]; then
echo "Using system keychain identity"
# Get certificate hash instead of name to avoid ambiguity
IDENTITY_HASH=$(security find-identity -v -p codesigning | grep "Developer ID Application" | head -1 | awk '{print $2}')
echo "Using certificate hash: $IDENTITY_HASH"
else
# Make sure keychain is unlocked
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
echo "Using custom keychain identity"
# Get certificate hash instead of name to avoid ambiguity
IDENTITY_HASH=$(security find-identity -v -p codesigning "$KEYCHAIN_PATH" | grep "Developer ID Application" | head -1 | awk '{print $2}')
echo "Using certificate hash: $IDENTITY_HASH"
fi
if [ -z "$IDENTITY_HASH" ]; then
echo "❌ No valid Developer ID Application certificate found"
if [ "${{ inputs.fallback-to-adhoc }}" = "true" ]; then
echo "Falling back to ad-hoc signing for testing..."
# Use ad-hoc identity as fallback
codesign --force --deep --verbose --options runtime --entitlements "${{ inputs.entitlements-file }}" --sign - --timestamp "${{ inputs.app-path }}"
echo "::set-output name=signed::adhoc"
else
echo "Skipping signing. Set fallback-to-adhoc=true to use ad-hoc signing instead."
echo "::set-output name=signed::none"
exit 1
fi
else
echo "Signing app bundle with Developer ID hash: $IDENTITY_HASH"
# Enhanced deep recursive signing for all binaries
echo "🔍 Performing deep recursive signing of all components..."
# First, find all .dylib files and sign them individually
echo "Signing all dynamic libraries (.dylib files)..."
find "${{ inputs.app-path }}" -name "*.dylib" | while read -r dylib; do
echo "Signing: $dylib"
codesign --force --verbose --options runtime --entitlements "${{ inputs.entitlements-file }}" --sign "$IDENTITY_HASH" --timestamp "$dylib" || echo "⚠️ Failed to sign: $dylib"
done
# Sign all .so files
echo "Signing all shared objects (.so files)..."
find "${{ inputs.app-path }}" -name "*.so" | while read -r so; do
echo "Signing: $so"
codesign --force --verbose --options runtime --entitlements "${{ inputs.entitlements-file }}" --sign "$IDENTITY_HASH" --timestamp "$so" || echo "⚠️ Failed to sign: $so"
done
# Sign all executable files (files with execute permission)
echo "Signing all executable files..."
find "${{ inputs.app-path }}" -type f -perm +111 -not -path "*.framework/*" -not -name "*.dylib" -not -name "*.so" | while read -r exe; do
echo "Signing executable: $exe"
codesign --force --verbose --options runtime --entitlements "${{ inputs.entitlements-file }}" --sign "$IDENTITY_HASH" --timestamp "$exe" || echo "⚠️ Failed to sign: $exe"
done
# Sign all frameworks
echo "Signing frameworks..."
find "${{ inputs.app-path }}" -path "*.framework" -type d | while read -r framework; do
echo "Signing framework: $framework"
codesign --force --verbose --options runtime --entitlements "${{ inputs.entitlements-file }}" --sign "$IDENTITY_HASH" --timestamp "$framework" || echo "⚠️ Failed to sign: $framework"
done
# Special handling for CrashReportClient.app
CRASH_REPORTER=$(find "${{ inputs.app-path }}" -path "*CrashReportClient.app" -type d | head -1)
if [ -n "$CRASH_REPORTER" ]; then
echo "🔍 Special handling for CrashReportClient.app: $CRASH_REPORTER"
# Sign CrashReportClient.app specifically with focus on hardened runtime
find "$CRASH_REPORTER" -type f -perm +111 | while read -r crash_bin; do
echo "Signing CrashReportClient binary: $crash_bin"
codesign --force --verbose --options runtime --entitlements "${{ inputs.entitlements-file }}" --sign "$IDENTITY_HASH" --timestamp "$crash_bin" || echo "⚠️ Failed to sign: $crash_bin"
done
echo "Signing the CrashReportClient.app bundle itself..."
codesign --force --deep --verbose --options runtime --entitlements "${{ inputs.entitlements-file }}" --sign "$IDENTITY_HASH" --timestamp "$CRASH_REPORTER" || echo "⚠️ Failed to sign CrashReportClient.app"
fi
# Sign any other nested app bundles
find "${{ inputs.app-path }}" -path "*.app" -type d | grep -v CrashReportClient | while read -r nested_app; do
if [ "$nested_app" != "${{ inputs.app-path }}" ]; then
echo "Signing nested app: $nested_app"
codesign --force --deep --verbose --options runtime --entitlements "${{ inputs.entitlements-file }}" --sign "$IDENTITY_HASH" --timestamp "$nested_app" || echo "⚠️ Failed to sign: $nested_app"
fi
done
# Final signing of the main bundle
echo "🔐 Performing final signing of the main app bundle..."
codesign --force --deep --verbose --options runtime --entitlements "${{ inputs.entitlements-file }}" --sign "$IDENTITY_HASH" --timestamp "${{ inputs.app-path }}"
echo "::set-output name=signed::identity"
fi
# Verify signing
echo "🔍 Verifying signature..."
codesign -vvv --deep --strict "${{ inputs.app-path }}"
# Check entitlements
echo "🔍 Checking entitlements..."
codesign -d --entitlements - "${{ inputs.app-path }}"
# Verify CrashReportClient
CRASH_REPORTER=$(find "${{ inputs.app-path }}" -path "*CrashReportClient.app" -type d | head -1)
if [ -n "$CRASH_REPORTER" ]; then
echo "🔍 Verifying CrashReportClient signature..."
codesign -vvv --deep --strict "$CRASH_REPORTER" || echo "⚠️ CrashReportClient may have verification issues"
echo "CrashReportClient entitlements:"
codesign -d --entitlements - "$CRASH_REPORTER" || echo "⚠️ Could not display CrashReportClient entitlements"
fi
- name: Notarize App
id: notarize
if: steps.sign.outputs.signed != 'none'
shell: bash
env:
APPLE_ID: ${{ inputs.notary-user }}
APP_PASSWORD: ${{ inputs.notary-password }}
API_KEY_ID: ${{ inputs.notary-api-key-id }}
API_ISSUER_ID: ${{ inputs.notary-api-key-issuer-id }}
API_KEY_PATH: ${{ inputs.notary-api-key-path }}
run: |
echo "📤 Notarizing app..."
# Set default output
echo "::set-output name=notarized::false"
# Get app name for zip file naming
APP_NAME=$(basename "${{ inputs.app-path }}" .app)
BUNDLE_ID="${{ inputs.bundle-id }}"
# If bundle ID is not provided, try to extract from Info.plist
if [ -z "$BUNDLE_ID" ]; then
if [ -f "${{ inputs.app-path }}/Contents/Info.plist" ]; then
BUNDLE_ID=$(/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "${{ inputs.app-path }}/Contents/Info.plist")
echo "Extracted bundle ID: $BUNDLE_ID"
else
BUNDLE_ID="com.luckyrobots.app"
echo "Using default bundle ID: $BUNDLE_ID"
fi
fi
# Check if we're using API key notarization method
if [ "${{ inputs.notarization-method }}" = "api-key" ] && [ -n "$API_KEY_ID" ] && [ -n "$API_ISSUER_ID" ] && [ -n "$API_KEY_PATH" ]; then
echo "Using App Store Connect API key for notarization..."
# Create directory for API key if API_KEY_PATH contains content
mkdir -p ~/private_keys
# Check if API_KEY_PATH is a path or content
if [[ "$API_KEY_PATH" == /* ]] && [ -f "$API_KEY_PATH" ]; then
# It's a path to a file
echo "Using API key from path: $API_KEY_PATH"
cp "$API_KEY_PATH" ~/private_keys/AuthKey_${API_KEY_ID}.p8
else
# It contains the key content
echo "Using API key from content"
echo "$API_KEY_PATH" > ~/private_keys/AuthKey_${API_KEY_ID}.p8
fi
# Create zip for notarization
ZIP_PATH="${APP_NAME}-notarize.zip"
ditto -c -k --keepParent "${{ inputs.app-path }}" "$ZIP_PATH"
echo "Submitting for notarization with API key..."
# First, submit without waiting for completion
SUBMIT_OUTPUT=$(xcrun notarytool submit "$ZIP_PATH" \
--key ~/private_keys/AuthKey_${API_KEY_ID}.p8 \
--key-id "$API_KEY_ID" \
--issuer "$API_ISSUER_ID" 2>&1)
SUBMIT_STATUS=$?
# Display output for debugging
echo "Notarization submission output:"
echo "$SUBMIT_OUTPUT"
echo "Submission exit status: $SUBMIT_STATUS"
# Check if submission was successful
if [ $SUBMIT_STATUS -ne 0 ]; then
echo "❌ Failed to submit for notarization. Exit code: $SUBMIT_STATUS"
exit 1
fi
# Extract submission ID for log retrieval
SUBMISSION_ID=$(echo "$SUBMIT_OUTPUT" | grep -o "id: [a-f0-9\-]*" | head -1 | cut -d ' ' -f 2)
if [ -z "$SUBMISSION_ID" ]; then
echo "❌ Could not extract submission ID from output. Notarization failed."
exit 1
fi
echo "Submission ID: $SUBMISSION_ID"
echo "Waiting for notarization to complete..."
# Now wait for the processing to complete
COMPLETE=false
MAX_ATTEMPTS=60 # Maximum number of attempts (60 * 30 seconds = 30 minutes max)
ATTEMPT=1
while [ "$COMPLETE" = "false" ] && [ $ATTEMPT -le $MAX_ATTEMPTS ]; do
echo "Checking notarization status (attempt $ATTEMPT of $MAX_ATTEMPTS)..."
INFO_OUTPUT=$(xcrun notarytool info "$SUBMISSION_ID" \
--key ~/private_keys/AuthKey_${API_KEY_ID}.p8 \
--key-id "$API_KEY_ID" \
--issuer "$API_ISSUER_ID" 2>&1)
INFO_STATUS=$?
echo "Status check output:"
echo "$INFO_OUTPUT"
# Check if the notarization is complete
if echo "$INFO_OUTPUT" | grep -q "status: Accepted"; then
echo "✅ Notarization completed successfully!"
COMPLETE=true
FINAL_STATUS="Accepted"
elif echo "$INFO_OUTPUT" | grep -q "status: Invalid"; then
echo "❌ Notarization failed with status: Invalid"
COMPLETE=true
FINAL_STATUS="Invalid"
elif echo "$INFO_OUTPUT" | grep -q "status: Rejected"; then
echo "❌ Notarization failed with status: Rejected"
COMPLETE=true
FINAL_STATUS="Rejected"
else
echo "Notarization still in progress. Waiting 30 seconds before checking again..."
sleep 30
ATTEMPT=$((ATTEMPT + 1))
fi
done
# Handle timeout
if [ "$COMPLETE" = "false" ]; then
echo "❌ Notarization timed out after $MAX_ATTEMPTS attempts."
exit 1
fi
# Handle completed notarization
if [ "$FINAL_STATUS" = "Accepted" ]; then
# Get logs for information (even though successful)
echo "📋 Getting notarization logs for information..."
LOGS_OUTPUT=$(xcrun notarytool log "$SUBMISSION_ID" \
--key ~/private_keys/AuthKey_${API_KEY_ID}.p8 \
--key-id "$API_KEY_ID" \
--issuer "$API_ISSUER_ID" 2>&1)
echo "==== NOTARIZATION LOG SUMMARY ===="
echo "$LOGS_OUTPUT" | head -20
echo "=================================="
# Staple the notarization ticket
echo "Stapling notarization ticket..."
xcrun stapler staple -v "${{ inputs.app-path }}"
STAPLE_STATUS=$?
if [ $STAPLE_STATUS -eq 0 ]; then
echo "✅ Stapling completed successfully!"
# Verify the stapling worked properly
echo "Verifying stapled ticket is properly attached..."
xcrun stapler validate -v "${{ inputs.app-path }}"
# Check if stapling metadata is correctly stored in xattr
echo "Checking app extended attributes..."
if command -v xattr &> /dev/null; then
xattr "${{ inputs.app-path }}" | grep -q "com.apple.provenance" || echo "⚠️ Warning: com.apple.provenance attribute not found"
fi
# Add special instructions for distribution
echo "📋 IMPORTANT DISTRIBUTION NOTE: When users download this app, they may still see Gatekeeper warnings."
echo "This happens because of the 'quarantine' extended attribute that browsers add to downloaded files."
echo "For proper distribution, consider the following options:"
echo "1. Use a DMG installer with a signed, notarized app inside"
echo "2. Add instructions for users on how to open a quarantined app (right-click > Open)"
echo "3. If distributing directly, use a distribution platform that preserves notarization tickets"
else
echo "⚠️ Stapling completed with status $STAPLE_STATUS (may still be valid)"
fi
# Verify notarization
echo "🔍 Verifying notarization..."
spctl --assess --verbose --type exec "${{ inputs.app-path }}"
echo "::set-output name=notarized::true"
else
# Get detailed logs for failed notarization
echo "📋 Fetching detailed logs for submission ID: $SUBMISSION_ID"
LOGS_OUTPUT=$(xcrun notarytool log "$SUBMISSION_ID" \
--key ~/private_keys/AuthKey_${API_KEY_ID}.p8 \
--key-id "$API_KEY_ID" \
--issuer "$API_ISSUER_ID" 2>&1)
echo "==== DETAILED NOTARIZATION LOGS ===="
echo "$LOGS_OUTPUT"
echo "=================================="
# Extract specific issues for easier debugging
echo "🔍 Extracting specific issues from logs..."
echo "$LOGS_OUTPUT" | grep -A 3 "issues"
# Show current bundle ID in Info.plist
echo "📋 Current bundle ID information:"
if [ -f "${{ inputs.app-path }}/Contents/Info.plist" ]; then
echo "Info.plist content for bundle ID:"
/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "${{ inputs.app-path }}/Contents/Info.plist" || echo "Could not read bundle ID from Info.plist"
echo "Full Info.plist excerpt:"
plutil -p "${{ inputs.app-path }}/Contents/Info.plist" | grep -i bundle
else
echo "Info.plist not found at expected location: ${{ inputs.app-path }}/Contents/Info.plist"
fi
# Check for mismatched bundle ID
if [ "$BUNDLE_ID" != "$(/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "${{ inputs.app-path }}/Contents/Info.plist" 2>/dev/null)" ]; then
echo "⚠️ WARNING: Bundle ID mismatch detected between workflow and app!"
echo " - Workflow/input bundle ID: $BUNDLE_ID"
echo " - Actual app bundle ID: $(/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "${{ inputs.app-path }}/Contents/Info.plist" 2>/dev/null || echo "Could not read")"
echo "This mismatch could cause notarization problems."
fi
# Check for code signature issues in internal components
echo "🔍 Checking for code signature issues in app components..."
find "${{ inputs.app-path }}" -type f -name "*.dylib" -o -name "*.so" | head -5 | while read -r lib; do
echo "Checking signature on: $lib"
codesign -vvv "$lib" || echo "⚠️ Signature issue with: $lib"
done
echo "❌ Notarization failed with status: $FINAL_STATUS"
exit 1
fi
# Clean up
rm -rf ~/private_keys
# Fall back to App-specific password if requested
elif [ "${{ inputs.notarization-method }}" = "app-password" ] && [ -n "$APPLE_ID" ] && [ -n "$APP_PASSWORD" ] && [ -n "$APPLE_TEAM_ID" ]; then
echo "Using App-specific password for notarization..."
# Create zip for notarization
ZIP_PATH="${APP_NAME}-notarize.zip"
ditto -c -k --keepParent "${{ inputs.app-path }}" "$ZIP_PATH"
echo "Submitting for notarization..."
# First, submit without waiting for completion
SUBMIT_OUTPUT=$(xcrun notarytool submit "$ZIP_PATH" \
--apple-id "$APPLE_ID" \
--password "$APP_PASSWORD" \
--team-id "$APPLE_TEAM_ID" 2>&1)
SUBMIT_STATUS=$?
# Display output for debugging
echo "Notarization submission output:"
echo "$SUBMIT_OUTPUT"
echo "Submission exit status: $SUBMIT_STATUS"
# Check if submission was successful
if [ $SUBMIT_STATUS -ne 0 ]; then
echo "❌ Failed to submit for notarization. Exit code: $SUBMIT_STATUS"
exit 1
fi
# Extract submission ID for log retrieval
SUBMISSION_ID=$(echo "$SUBMIT_OUTPUT" | grep -o "id: [a-f0-9\-]*" | head -1 | cut -d ' ' -f 2)
if [ -z "$SUBMISSION_ID" ]; then
echo "❌ Could not extract submission ID from output. Notarization failed."
exit 1
fi
echo "Submission ID: $SUBMISSION_ID"
echo "Waiting for notarization to complete..."
# Now wait for the processing to complete
COMPLETE=false
MAX_ATTEMPTS=60 # Maximum number of attempts (60 * 30 seconds = 30 minutes max)
ATTEMPT=1
while [ "$COMPLETE" = "false" ] && [ $ATTEMPT -le $MAX_ATTEMPTS ]; do
echo "Checking notarization status (attempt $ATTEMPT of $MAX_ATTEMPTS)..."
INFO_OUTPUT=$(xcrun notarytool info "$SUBMISSION_ID" \
--apple-id "$APPLE_ID" \
--password "$APP_PASSWORD" \
--team-id "$APPLE_TEAM_ID" 2>&1)
INFO_STATUS=$?
echo "Status check output:"
echo "$INFO_OUTPUT"
# Check if the notarization is complete
if echo "$INFO_OUTPUT" | grep -q "status: Accepted"; then
echo "✅ Notarization completed successfully!"
COMPLETE=true
FINAL_STATUS="Accepted"
elif echo "$INFO_OUTPUT" | grep -q "status: Invalid"; then
echo "❌ Notarization failed with status: Invalid"
COMPLETE=true
FINAL_STATUS="Invalid"
elif echo "$INFO_OUTPUT" | grep -q "status: Rejected"; then
echo "❌ Notarization failed with status: Rejected"
COMPLETE=true
FINAL_STATUS="Rejected"
else
echo "Notarization still in progress. Waiting 30 seconds before checking again..."
sleep 30
ATTEMPT=$((ATTEMPT + 1))
fi
done
# Handle timeout
if [ "$COMPLETE" = "false" ]; then
echo "❌ Notarization timed out after $MAX_ATTEMPTS attempts."
exit 1
fi
# Handle completed notarization
if [ "$FINAL_STATUS" = "Accepted" ]; then
# Get logs for information (even though successful)
echo "📋 Getting notarization logs for information..."
LOGS_OUTPUT=$(xcrun notarytool log "$SUBMISSION_ID" \
--apple-id "$APPLE_ID" \
--password "$APP_PASSWORD" \
--team-id "$APPLE_TEAM_ID" 2>&1)
echo "==== NOTARIZATION LOG SUMMARY ===="
echo "$LOGS_OUTPUT" | head -20
echo "=================================="
# Staple the notarization ticket
echo "Stapling notarization ticket..."
xcrun stapler staple -v "${{ inputs.app-path }}"
STAPLE_STATUS=$?
if [ $STAPLE_STATUS -eq 0 ]; then
echo "✅ Stapling completed successfully!"
# Verify the stapling worked properly
echo "Verifying stapled ticket is properly attached..."
xcrun stapler validate -v "${{ inputs.app-path }}"
# Check if stapling metadata is correctly stored in xattr
echo "Checking app extended attributes..."
if command -v xattr &> /dev/null; then
xattr "${{ inputs.app-path }}" | grep -q "com.apple.provenance" || echo "⚠️ Warning: com.apple.provenance attribute not found"
fi
# Add special instructions for distribution
echo "📋 IMPORTANT DISTRIBUTION NOTE: When users download this app, they may still see Gatekeeper warnings."
echo "This happens because of the 'quarantine' extended attribute that browsers add to downloaded files."
echo "For proper distribution, consider the following options:"
echo "1. Use a DMG installer with a signed, notarized app inside"
echo "2. Add instructions for users on how to open a quarantined app (right-click > Open)"
echo "3. If distributing directly, use a distribution platform that preserves notarization tickets"
else
echo "⚠️ Stapling completed with status $STAPLE_STATUS (may still be valid)"
fi
# Verify notarization
echo "🔍 Verifying notarization..."
spctl --assess --verbose --type exec "${{ inputs.app-path }}"
echo "::set-output name=notarized::true"
else
# Get detailed logs for failed notarization
echo "📋 Fetching detailed logs for submission ID: $SUBMISSION_ID"
LOGS_OUTPUT=$(xcrun notarytool log "$SUBMISSION_ID" \
--apple-id "$APPLE_ID" \
--password "$APP_PASSWORD" \
--team-id "$APPLE_TEAM_ID" 2>&1)
echo "==== DETAILED NOTARIZATION LOGS ===="
echo "$LOGS_OUTPUT"
echo "=================================="
# Extract specific issues for easier debugging
echo "🔍 Extracting specific issues from logs..."
echo "$LOGS_OUTPUT" | grep -A 3 "issues"
# Show current bundle ID in Info.plist
echo "📋 Current bundle ID information:"
if [ -f "${{ inputs.app-path }}/Contents/Info.plist" ]; then
echo "Info.plist content for bundle ID:"
/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "${{ inputs.app-path }}/Contents/Info.plist" || echo "Could not read bundle ID from Info.plist"
echo "Full Info.plist excerpt:"
plutil -p "${{ inputs.app-path }}/Contents/Info.plist" | grep -i bundle
else
echo "Info.plist not found at expected location: ${{ inputs.app-path }}/Contents/Info.plist"
fi
echo "❌ Notarization failed with status: $FINAL_STATUS"
exit 1
fi
else
echo "⚠️ Missing notarization credentials. Skipping notarization."
echo "For App Store Connect API key method, set these inputs:"
echo " - notarization-method: api-key"
echo " - notary-api-key-id: Your API key ID"
echo " - notary-api-key-issuer-id: Your API issuer ID"
echo " - notary-api-key-path: Path to or content of your p8 file"
echo ""
echo "For App-specific password method, set these inputs:"
echo " - notarization-method: app-password"
echo " - notary-user: Your Apple ID (email)"
echo " - notary-password: Your app-specific password"
echo " - team-id: Your Apple Developer team ID"
fi
- name: Package App
id: package
if: steps.sign.outputs.signed != 'none'
shell: bash
run: |
echo "📦 Packaging signed app..."
# Get app name for zip file naming
APP_NAME=$(basename "${{ inputs.app-path }}" .app)
if [ "${{ steps.notarize.outputs.notarized }}" = "true" ]; then
PACKAGE_SUFFIX="Signed-Notarized"
echo "Creating distribution package with notarized app..."
else
PACKAGE_SUFFIX="Signed"
echo "Creating distribution package with signed app..."
fi
# Create zip package
ZIP_FILE="${APP_NAME}-${PACKAGE_SUFFIX}.zip"
ditto -c -k --keepParent "${{ inputs.app-path }}" "$ZIP_FILE"
echo "✅ Created ZIP package: $ZIP_FILE"
# Check if we can create DMG (hdiutil is available)
if command -v hdiutil &> /dev/null; then
# Create DMG package (much better for distribution)
DMG_FILE="${APP_NAME}-${PACKAGE_SUFFIX}.dmg"
echo "Creating DMG distribution package..."
# Create temporary folder for DMG contents
DMG_TMP_DIR=$(mktemp -d)
cp -R "${{ inputs.app-path }}" "$DMG_TMP_DIR/"
# Optional: Add README or instructions
echo "# Installation Instructions\n\nDrag the application to your Applications folder to install." > "$DMG_TMP_DIR/README.txt"
# Create DMG file with the app
hdiutil create -volname "${APP_NAME}" -srcfolder "$DMG_TMP_DIR" -ov -format UDZO "$DMG_FILE"
if [ -f "$DMG_FILE" ]; then
echo "✅ Created DMG package: $DMG_FILE"
# Use DMG as the primary package if available
echo "::set-output name=package-path::$DMG_FILE"
echo "::set-output name=zip-package-path::$ZIP_FILE"
else
echo "⚠️ Failed to create DMG, falling back to ZIP package"
echo "::set-output name=package-path::$ZIP_FILE"
fi
# Clean up temp directory
rm -rf "$DMG_TMP_DIR"
else
echo "hdiutil not available, skipping DMG creation"
echo "::set-output name=package-path::$ZIP_FILE"
fi
- name: Cleanup
if: always()
shell: bash
run: |
echo "🧹 Cleaning up..."
security delete-keychain "$KEYCHAIN_PATH" 2>/dev/null || true
rm -f *-notarize.zip || true
echo "✅ Cleanup complete"