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" # Extract submission ID for log retrieval if needed SUBMISSION_ID=$(echo "$NOTARY_OUTPUT" | grep -o "id: [a-f0-9\-]*" | head -1 | cut -d ' ' -f 2) echo "Submission ID: $SUBMISSION_ID" # Check for invalid status and get detailed logs if [ $NOTARY_STATUS -eq 0 ] && echo "$NOTARY_OUTPUT" | grep -q "Invalid"; then echo "โš ๏ธ Notarization returned Invalid status. Checking detailed logs..." if [ -n "$SUBMISSION_ID" ]; then 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" else echo "โŒ Could not extract submission ID from notarization output" fi fi # 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 above for details" # 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 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" # Extract submission ID for log retrieval if needed SUBMISSION_ID=$(echo "$NOTARY_OUTPUT" | grep -o "id: [a-f0-9\-]*" | head -1 | cut -d ' ' -f 2) echo "Submission ID: $SUBMISSION_ID" # Check for invalid status and get detailed logs if [ $NOTARY_STATUS -eq 0 ] && echo "$NOTARY_OUTPUT" | grep -q "Invalid"; then echo "โš ๏ธ Notarization returned Invalid status. Checking detailed logs..." if [ -n "$SUBMISSION_ID" ]; then 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" else echo "โŒ Could not extract submission ID from notarization output" fi fi # 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 above for details" # 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 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"