Some checks failed
Test macOS Build Action / test-macos-build (push) Failing after 14m22s
649 lines
29 KiB
YAML
649 lines
29 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 "${{ 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
|
|
# 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 "${{ 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
|
|
# 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
|
|
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" |