Ozgur Ersoy db463a2ece
All checks were successful
Test macOS Build Action / test-macos-build (push) Successful in 44m17s
fix(actions): enhance notarization process with detailed output and error handling
2025-04-14 18:07:06 +02:00

380 lines
15 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"
# Sign the app bundle using the hash
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 }}"
- 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..."
# Capture output and exit status of notarization
NOTARY_OUTPUT=$(xcrun notarytool submit "$ZIP_PATH" \
--key ~/private_keys/AuthKey_${API_KEY_ID}.p8 \
--key-id "$API_KEY_ID" \
--issuer "$API_ISSUER_ID" \
--wait 2>&1)
NOTARY_STATUS=$?
# Display output for debugging
echo "Notarization command output:"
echo "$NOTARY_OUTPUT"
echo "Notarization exit status: $NOTARY_STATUS"
# Enhanced check for notarization success
if [ $NOTARY_STATUS -eq 0 ] && echo "$NOTARY_OUTPUT" | grep -q -E "success|accepted"; then
echo "✅ Notarization completed successfully!"
# Staple the notarization ticket
echo "Stapling notarization ticket..."
xcrun stapler staple "${{ inputs.app-path }}"
STAPLE_STATUS=$?
if [ $STAPLE_STATUS -eq 0 ]; then
echo "✅ Stapling completed successfully!"
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
echo "❌ Notarization failed or did not complete properly"
echo "Please check the notarization logs for details"
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..."
# Capture output and exit status of notarization
NOTARY_OUTPUT=$(xcrun notarytool submit "$ZIP_PATH" \
--apple-id "$APPLE_ID" \
--password "$APP_PASSWORD" \
--team-id "$APPLE_TEAM_ID" \
--wait 2>&1)
NOTARY_STATUS=$?
# Display output for debugging
echo "Notarization command output:"
echo "$NOTARY_OUTPUT"
echo "Notarization exit status: $NOTARY_STATUS"
# Enhanced check for notarization success
if [ $NOTARY_STATUS -eq 0 ] && echo "$NOTARY_OUTPUT" | grep -q -E "success|accepted"; then
echo "✅ Notarization completed successfully!"
# Staple the notarization ticket
echo "Stapling notarization ticket..."
xcrun stapler staple "${{ inputs.app-path }}"
STAPLE_STATUS=$?
if [ $STAPLE_STATUS -eq 0 ]; then
echo "✅ Stapling completed successfully!"
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
echo "❌ Notarization failed or did not complete properly"
echo "Please check the notarization logs for details"
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
ZIP_FILE="${APP_NAME}-Signed-Notarized.zip"
echo "Creating distribution package with notarized app..."
else
ZIP_FILE="${APP_NAME}-Signed.zip"
echo "Creating distribution package with signed app..."
fi
# Create zip package
ditto -c -k --keepParent "${{ inputs.app-path }}" "$ZIP_FILE"
echo "✅ Created package: $ZIP_FILE"
echo "::set-output name=package-path::$ZIP_FILE"
- 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"