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"