From 1c7e12e27989e2019a257f12ed81482e5df6e740 Mon Sep 17 00:00:00 2001 From: Ozgur Ersoy Date: Wed, 16 Apr 2025 01:27:26 +0200 Subject: [PATCH] fix(actions): enhance macOS notarization workflow by adding debug logging, build cache management, and improved artifact handling --- .gitea/actions/macos-notarize/action.yml | 1430 +++++++++------------- .gitea/workflows/test-macos-build.yml | 56 +- 2 files changed, 653 insertions(+), 833 deletions(-) diff --git a/.gitea/actions/macos-notarize/action.yml b/.gitea/actions/macos-notarize/action.yml index 98a58f9a..e77e590b 100644 --- a/.gitea/actions/macos-notarize/action.yml +++ b/.gitea/actions/macos-notarize/action.yml @@ -1,936 +1,704 @@ -name: "macOS Sign and Notarize" -description: "Signs and notarizes macOS applications with Developer ID certificate" +name: macOS Notarize +description: 'Signs and notarizes a macOS application with Apple certificates' author: moersoy inputs: app-path: - description: "Path to the app bundle (.app)" + description: 'Path to the .app bundle to sign' required: true entitlements-file: - description: "Path to the entitlements file (.entitlements)" - required: true + description: 'Path to entitlements file to use for signing' + required: false + default: '' team-id: - description: "Apple Developer Team ID" + description: 'Apple Developer Team ID' required: true certificate-base64: - description: "Base64-encoded Developer ID Application certificate (.p12)" + description: 'Base64-encoded certificate (P12 file)' required: true certificate-password: - description: "Password for the Developer ID Application certificate" + description: 'Certificate password' required: true notarization-method: - description: "Method to use for notarization: 'api-key' or 'app-password'" + 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)" + default: 'api-key' + app-password: + description: 'App-specific password for Apple ID (required if using app-password method)' required: false - notary-password: - description: "App-specific password for notarization (for app-password method)" + default: '' + apple-id: + description: 'Apple ID email (required if using app-password method)' required: false + default: '' notary-api-key-id: - description: "API Key ID for notarization (for api-key method)" + description: 'App Store Connect API Key ID (required if using api-key method)' required: false + default: '' notary-api-key-issuer-id: - description: "API Issuer ID for notarization (for api-key method)" + description: 'App Store Connect API Key Issuer ID (required if using api-key method)' required: false + default: '' notary-api-key-path: - description: "Path to or content of the API Key .p8 file (for api-key method)" + description: 'App Store Connect API Key file content (base64 encoded) (required if using api-key method)' required: false + default: '' bundle-id: - description: "App bundle identifier (com.example.app)" + description: 'Bundle ID of the app' required: false + default: '' fallback-to-adhoc: - description: "Whether to fall back to ad-hoc signing if certificate is invalid" + description: 'Fallback to ad-hoc signing if no certificate is available' required: false - default: "true" + default: 'true' outputs: signed: - description: "Whether the app was signed (identity, adhoc, or none)" - value: ${{ steps.sign.outputs.signed }} + description: 'Signing status (true, ad-hoc, none)' + value: ${{ steps.set-outputs.outputs.signed }} notarized: - description: "Whether the app was notarized (true or false)" - value: ${{ steps.notarize.outputs.notarized }} + description: 'Notarization status (true, false)' + value: ${{ steps.set-outputs.outputs.notarized }} + app-path: + description: 'Path to the signed app bundle' + value: ${{ steps.set-outputs.outputs.app-path }} + zip-path: + description: 'Path to the packaged .ZIP file' + value: ${{ steps.set-outputs.outputs.zip-path }} package-path: - description: "Path to the final package" - value: ${{ steps.package.outputs.package-path }} + description: 'Path to the packaged .DMG file' + value: ${{ steps.set-outputs.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 }} + - name: Setup debug environment 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" + # Create debug directory if env variable is set + if [[ -n "$DEBUG_LOG_PATH" ]]; then + mkdir -p "$(dirname "$DEBUG_LOG_PATH")" + touch "$DEBUG_LOG_PATH" + echo "Debug logging enabled to: $DEBUG_LOG_PATH" | tee -a "$DEBUG_LOG_PATH" fi - # Store keychain variables for later steps - echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> $GITHUB_ENV + # Define a debug function + debug_log() { + echo "DEBUG: $1" + if [[ -n "$DEBUG_LOG_PATH" ]]; then + echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$DEBUG_LOG_PATH" + fi + } + # Export the function for use in subsequent steps + export -f debug_log + + debug_log "Starting macOS notarize action" + debug_log "App path: ${{ inputs.app-path }}" + debug_log "Team ID: ${{ inputs.team-id }}" + debug_log "Notarization method: ${{ inputs.notarization-method }}" + debug_log "Bundle ID: ${{ inputs.bundle-id }}" + shell: bash + + - name: Set up variables + id: setup + run: | + # Debugging info + debug_log "Setting up variables" + + # Generate unique name for keychain + KEYCHAIN_NAME="build-keychain-$(uuidgen)" + KEYCHAIN_PASSWORD="$(uuidgen)" + echo "KEYCHAIN_NAME=$KEYCHAIN_NAME" >> $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" + # Set paths + echo "APP_PATH=${{ inputs.app-path }}" >> $GITHUB_ENV - - name: Sign App - id: sign - shell: bash - run: | - echo "๐Ÿ” Signing app with Developer ID certificate..." + # Generate working directory for temp files + WORK_DIR="$(mktemp -d)" + echo "WORK_DIR=$WORK_DIR" >> $GITHUB_ENV - # 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" + # Set bundle id (from input or extract from app) + if [[ -n "${{ inputs.bundle-id }}" ]]; then + BUNDLE_ID="${{ inputs.bundle-id }}" 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" + BUNDLE_ID=$(/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "${{ inputs.app-path }}/Contents/Info.plist") + fi + echo "BUNDLE_ID=$BUNDLE_ID" >> $GITHUB_ENV + + # Get app name from bundle path + APP_NAME=$(basename "${{ inputs.app-path }}" .app) + echo "APP_NAME=$APP_NAME" >> $GITHUB_ENV + + # Set output directory + OUTPUT_DIR="$(pwd)/PackagedReleases" + mkdir -p "$OUTPUT_DIR" + echo "OUTPUT_DIR=$OUTPUT_DIR" >> $GITHUB_ENV + + # Set package paths + ZIP_PATH="$OUTPUT_DIR/${APP_NAME}.zip" + DMG_PATH="$OUTPUT_DIR/${APP_NAME}.dmg" + echo "ZIP_PATH=$ZIP_PATH" >> $GITHUB_ENV + echo "DMG_PATH=$DMG_PATH" >> $GITHUB_ENV + + # Set notarization variables based on method + if [[ "${{ inputs.notarization-method }}" == "api-key" ]]; then + echo "Using API key method for notarization" + + # Create API key file + API_KEY_FILE="$WORK_DIR/api_key.p8" + echo "${{ inputs.notary-api-key-path }}" | base64 --decode > "$API_KEY_FILE" + echo "API_KEY_FILE=$API_KEY_FILE" >> $GITHUB_ENV + + # Verify API key file exists + if [[ ! -f "$API_KEY_FILE" ]]; then + debug_log "ERROR: API key file could not be created" + exit 1 + fi + + debug_log "API key file created at: $API_KEY_FILE" + debug_log "API key ID: ${{ inputs.notary-api-key-id }}" + debug_log "API key issuer ID: ${{ inputs.notary-api-key-issuer-id }}" + else + echo "Using app-specific password method for notarization" + debug_log "Apple ID: ${{ inputs.apple-id }}" + fi + shell: bash + + - name: Setup keychain + id: setup-keychain + run: | + debug_log "Setting up keychain" + + # Create temporary keychain + security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME" + security default-keychain -s "$KEYCHAIN_NAME" + security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME" + security set-keychain-settings -t 3600 -u "$KEYCHAIN_NAME" + + # Create certificate file + CERTIFICATE_PATH="$WORK_DIR/certificate.p12" + echo "${{ inputs.certificate-base64 }}" | base64 --decode > "$CERTIFICATE_PATH" + + # Add to keychain + debug_log "Importing certificate into keychain" + security import "$CERTIFICATE_PATH" -k "$KEYCHAIN_NAME" -P "${{ inputs.certificate-password }}" -T /usr/bin/codesign + + # Allow codesign to access keychain items + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME" + + # Verify certificate was imported + security find-identity -v "$KEYCHAIN_NAME" | grep "Developer ID Application" + IDENTITY_RESULT=$? + + if [ $IDENTITY_RESULT -eq 0 ]; then + debug_log "Certificate imported successfully" + SIGNING_IDENTITY="Developer ID Application: ${{ inputs.team-id }}" + echo "SIGNING_IDENTITY=$SIGNING_IDENTITY" >> $GITHUB_ENV + echo "CERTIFICATE_AVAILABLE=true" >> $GITHUB_ENV + else + debug_log "WARNING: No Developer ID Application certificate found" + if [[ "${{ inputs.fallback-to-adhoc }}" == "true" ]]; then + debug_log "Falling back to ad-hoc signing" + echo "CERTIFICATE_AVAILABLE=adhoc" >> $GITHUB_ENV + else + debug_log "Not falling back to ad-hoc signing as specified" + echo "CERTIFICATE_AVAILABLE=false" >> $GITHUB_ENV + fi + fi + shell: bash + + - name: Sign application + id: sign-app + run: | + debug_log "Starting application signing process" + + # Check if certificate is available + if [[ "$CERTIFICATE_AVAILABLE" == "false" ]]; then + debug_log "No certificate available and fallback disabled. Skipping signing." + echo "SIGNING_RESULT=none" >> $GITHUB_ENV + exit 0 fi - if [ -z "$IDENTITY_HASH" ]; then - echo "โŒ No valid Developer ID Application certificate found" + # Sign the app + if [[ "$CERTIFICATE_AVAILABLE" == "true" ]]; then + debug_log "Signing with Developer ID certificate" - 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" + # First remove existing signatures + debug_log "Removing existing signatures..." + codesign --remove-signature "$APP_PATH" || true + + # Sign all dynamic libraries and frameworks + debug_log "Signing embedded binaries and frameworks..." + find "$APP_PATH/Contents/MacOS" -type f -name "*.dylib" -exec codesign --force --timestamp --options runtime --entitlements "$ENTITLEMENTS_PATH" --sign "$SIGNING_IDENTITY" {} \; + find "$APP_PATH/Contents/Frameworks" -type f -depth 1 -exec codesign --force --timestamp --options runtime --entitlements "$ENTITLEMENTS_PATH" --sign "$SIGNING_IDENTITY" {} \; + find "$APP_PATH/Contents/Frameworks" -name "*.framework" -exec codesign --force --timestamp --options runtime --entitlements "$ENTITLEMENTS_PATH" --sign "$SIGNING_IDENTITY" {} \; + + # Sign all executables + debug_log "Signing executables..." + find "$APP_PATH/Contents/MacOS" -type f -exec codesign --force --timestamp --options runtime --entitlements "$ENTITLEMENTS_PATH" --sign "$SIGNING_IDENTITY" {} \; + + # Sign app bundle + debug_log "Signing main app bundle..." + codesign --force --timestamp --options runtime --entitlements "$ENTITLEMENTS_PATH" --sign "$SIGNING_IDENTITY" "$APP_PATH" + + SIGN_RESULT=$? + if [ $SIGN_RESULT -eq 0 ]; then + debug_log "App signed successfully with Developer ID" + echo "SIGNING_RESULT=true" >> $GITHUB_ENV else - echo "Skipping signing. Set fallback-to-adhoc=true to use ad-hoc signing instead." - echo "::set-output name=signed::none" + debug_log "App signing failed with Developer ID" + echo "SIGNING_RESULT=false" >> $GITHUB_ENV + exit 1 + fi + + elif [[ "$CERTIFICATE_AVAILABLE" == "adhoc" ]]; then + debug_log "Signing with ad-hoc identity (not suitable for distribution)" + + # Remove existing signatures + codesign --remove-signature "$APP_PATH" || true + + # Sign with ad-hoc identity + codesign --force --timestamp --options runtime --entitlements "$ENTITLEMENTS_PATH" --sign - "$APP_PATH" + + SIGN_RESULT=$? + if [ $SIGN_RESULT -eq 0 ]; then + debug_log "App signed successfully with ad-hoc identity" + echo "SIGNING_RESULT=ad-hoc" >> $GITHUB_ENV + else + debug_log "App signing failed with ad-hoc identity" + echo "SIGNING_RESULT=false" >> $GITHUB_ENV 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" + debug_log "Unexpected certificate state. Skipping signing." + echo "SIGNING_RESULT=none" >> $GITHUB_ENV 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' + debug_log "Verifying app signature..." + codesign -dvv "$APP_PATH" 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 }} + + - name: Verify notarization and stapling + id: verify-notarization + if: env.SIGNING_RESULT == 'true' run: | - echo "๐Ÿ“ค Notarizing app..." + debug_log "Verifying app signature and code requirements before notarization" - # 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 + # Verify code signature + codesign --verify --verbose "$APP_PATH" + if [ $? -ne 0 ]; then + debug_log "Error: App signature verification failed" + # Don't exit, just log the error + else + debug_log "App signature verification passed" 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..." + # Check app for code requirements + codesign --display --requirements "$APP_PATH" + if [ $? -ne 0 ]; then + debug_log "Error: App doesn't meet requirements" + # Don't exit, just log the error + else + debug_log "App meets code requirements" + fi + shell: bash + + - name: Notarize application + id: notarize-app + if: env.SIGNING_RESULT == 'true' + run: | + debug_log "Starting notarization process" + + # Create ZIP for notarization + debug_log "Creating ZIP archive for notarization" + ditto -c -k --keepParent "$APP_PATH" "$ZIP_PATH" + if [ $? -ne 0 ]; then + debug_log "Error creating ZIP archive" + echo "NOTARIZATION_RESULT=false" >> $GITHUB_ENV + exit 1 + fi + + # Notarize the app + if [[ "${{ inputs.notarization-method }}" == "api-key" ]]; then + debug_log "Notarizing with API key method" - # Create directory for API key if API_KEY_PATH contains content - mkdir -p ~/private_keys + # Submit for notarization + debug_log "Submitting app for notarization..." + xcrun notarytool submit "$ZIP_PATH" \ + --key "$API_KEY_FILE" \ + --key-id "${{ inputs.notary-api-key-id }}" \ + --issuer "${{ inputs.notary-api-key-issuer-id }}" \ + --wait > "$WORK_DIR/notarization_output.txt" 2>&1 + + cat "$WORK_DIR/notarization_output.txt" | tee -a "$DEBUG_LOG_PATH" + + REQUEST_STATUS=$(grep -o "status: .*" "$WORK_DIR/notarization_output.txt" | cut -d ' ' -f2) - # 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 + if [[ "$REQUEST_STATUS" == "Accepted" ]]; then + debug_log "Notarization successful" + echo "NOTARIZATION_RESULT=true" >> $GITHUB_ENV else - # It contains the key content - echo "Using API key from content" - echo "$API_KEY_PATH" > ~/private_keys/AuthKey_${API_KEY_ID}.p8 + debug_log "Notarization failed or timed out" + cat "$WORK_DIR/notarization_output.txt" + echo "NOTARIZATION_RESULT=false" >> $GITHUB_ENV fi - # Create zip for notarization - ZIP_PATH="${APP_NAME}-notarize.zip" - ditto -c -k --keepParent "${{ inputs.app-path }}" "$ZIP_PATH" + else + debug_log "Notarizing with app-specific password method" - 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)..." + # Submit for notarization + debug_log "Submitting app for notarization..." + xcrun altool --notarize-app \ + --primary-bundle-id "$BUNDLE_ID" \ + --username "${{ inputs.apple-id }}" \ + --password "${{ inputs.app-password }}" \ + --file "$ZIP_PATH" \ + > "$WORK_DIR/notarization_output.txt" 2>&1 - 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=$? + cat "$WORK_DIR/notarization_output.txt" | tee -a "$DEBUG_LOG_PATH" - 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 + REQUEST_UUID=$(grep -o "RequestUUID = .*" "$WORK_DIR/notarization_output.txt" | cut -d ' ' -f3) - # 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) + if [[ -n "$REQUEST_UUID" ]]; then + debug_log "Notarization request submitted, UUID: $REQUEST_UUID" - echo "==== NOTARIZATION LOG SUMMARY ====" - echo "$LOGS_OUTPUT" | head -20 - echo "==================================" + # Wait for notarization to complete + debug_log "Waiting for notarization to complete..." + TIMEOUT=30 # 30 minutes timeout + COUNT=0 + NOTARIZATION_STATUS="in progress" - # 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!" + while [[ "$NOTARIZATION_STATUS" == "in progress" && $COUNT -lt $TIMEOUT ]]; do + sleep 60 - # Verify the stapling worked properly - echo "Verifying stapled ticket is properly attached..." - xcrun stapler validate -v "${{ inputs.app-path }}" + xcrun altool --notarization-info "$REQUEST_UUID" \ + --username "${{ inputs.apple-id }}" \ + --password "${{ inputs.app-password }}" \ + > "$WORK_DIR/notarization_info.txt" 2>&1 + + cat "$WORK_DIR/notarization_info.txt" | tee -a "$DEBUG_LOG_PATH" + + NOTARIZATION_STATUS=$(grep -o "Status: .*" "$WORK_DIR/notarization_info.txt" | cut -d ':' -f2 | xargs) - # 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" + debug_log "Notarization status: $NOTARIZATION_STATUS" + COUNT=$((COUNT+1)) 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" + if [[ "$NOTARIZATION_STATUS" == "success" ]]; then + debug_log "Notarization successful" + echo "NOTARIZATION_RESULT=true" >> $GITHUB_ENV 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" + debug_log "Notarization failed or timed out: $NOTARIZATION_STATUS" + if [[ -f "$WORK_DIR/notarization_info.txt" ]]; then + cat "$WORK_DIR/notarization_info.txt" 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)" + echo "NOTARIZATION_RESULT=false" >> $GITHUB_ENV 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 + debug_log "Notarization submission failed, no UUID returned" + cat "$WORK_DIR/notarization_output.txt" + echo "NOTARIZATION_RESULT=false" >> $GITHUB_ENV 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 + + - name: Staple notarization ticket + id: staple-ticket + if: env.SIGNING_RESULT == 'true' && env.NOTARIZATION_RESULT == 'true' run: | - echo "๐Ÿ“ฆ Packaging signed app..." + debug_log "Stapling notarization ticket to app" - # Get app name for zip file naming - APP_NAME=$(basename "${{ inputs.app-path }}" .app) + # Staple the ticket + xcrun stapler staple "$APP_PATH" + STAPLE_RESULT=$? - if [ "${{ steps.notarize.outputs.notarized }}" = "true" ]; then - PACKAGE_SUFFIX="Signed-Notarized" - echo "Creating distribution package with notarized app..." + if [ $STAPLE_RESULT -eq 0 ]; then + debug_log "Notarization ticket stapled successfully" + echo "STAPLING_RESULT=true" >> $GITHUB_ENV + + # Verify stapling + debug_log "Verifying notarization stapling" + xcrun stapler validate "$APP_PATH" + if [ $? -eq 0 ]; then + debug_log "Stapling validation successful" + else + debug_log "Stapling validation failed, but continuing" + fi else - PACKAGE_SUFFIX="Signed" - echo "Creating distribution package with signed app..." + debug_log "Stapling failed" + echo "STAPLING_RESULT=false" >> $GITHUB_ENV + fi + shell: bash + + - name: Remove quarantine attribute + id: remove-quarantine + if: env.SIGNING_RESULT != 'none' + run: | + debug_log "Removing quarantine attribute from app" + + # Create helper script + QUARANTINE_SCRIPT="$WORK_DIR/remove_quarantine.sh" + cat > "$QUARANTINE_SCRIPT" << 'EOF' +#!/bin/bash +# Removes the quarantine attribute from app and all its contents +echo "Removing quarantine attribute from all files..." +find "$1" -exec xattr -d com.apple.quarantine {} \; 2>/dev/null || true +echo "Quarantine attributes removed" +EOF + chmod +x "$QUARANTINE_SCRIPT" + + # Remove quarantine attribute + "$QUARANTINE_SCRIPT" "$APP_PATH" + + debug_log "Quarantine attribute removal completed" + shell: bash + + - name: Package signed app + id: package-app + run: | + debug_log "Packaging the signed app" + + # Check if we should use create-dmg if available + if command -v create-dmg &> /dev/null; then + debug_log "Using create-dmg for DMG creation" + + # Create a temporary directory for DMG contents + DMG_TEMP_DIR="$WORK_DIR/dmg-contents" + mkdir -p "$DMG_TEMP_DIR" + + # Copy the app to the temporary directory + cp -R "$APP_PATH" "$DMG_TEMP_DIR/" + + # Create instructions text file + echo "Drag the application to the Applications folder to install it." > "$DMG_TEMP_DIR/README.txt" + + # Create symlink to Applications folder + ln -s /Applications "$DMG_TEMP_DIR/Applications" + + # Use create-dmg to create a more beautiful DMG + create-dmg \ + --volname "$APP_NAME" \ + --window-pos 200 120 \ + --window-size 800 400 \ + --icon-size 100 \ + --app-drop-link 600 185 \ + --icon "$APP_NAME.app" 200 185 \ + --hide-extension "$APP_NAME.app" \ + --add-file "README.txt" 400 185 \ + --no-internet-enable \ + "$DMG_PATH" \ + "$DMG_TEMP_DIR" + + DMG_CREATE_RESULT=$? + + elif command -v hdiutil &> /dev/null; then + debug_log "Using hdiutil for DMG creation" + + # Create DMG using hdiutil + hdiutil create -volname "$APP_NAME" -srcfolder "$APP_PATH" -ov -format UDZO "$DMG_PATH" + DMG_CREATE_RESULT=$? + + else + debug_log "Neither create-dmg nor hdiutil available. Cannot create DMG." + DMG_CREATE_RESULT=1 fi - # Create zip package (necessary for notarization process only) - ZIP_FILE="${APP_NAME}-${PACKAGE_SUFFIX}.zip" - ditto -c -k --keepParent "${{ inputs.app-path }}" "$ZIP_FILE" - echo "โœ… Created temporary ZIP package for notarization: $ZIP_FILE" + # Check DMG creation result + if [ $DMG_CREATE_RESULT -eq 0 ]; then + debug_log "DMG package created successfully at: $DMG_PATH" + echo "DMG_CREATED=true" >> $GITHUB_ENV + else + debug_log "DMG creation failed" + echo "DMG_CREATED=false" >> $GITHUB_ENV + fi - # Verify stapling on the app before packaging - echo "๐Ÿ” Verifying app notarization and stapling..." - xcrun stapler validate "${{ inputs.app-path }}" || echo "โš ๏ธ App stapling verification failed" - spctl -a -vvv --type exec "${{ inputs.app-path }}" || echo "โš ๏ธ App notarization verification failed" - - # 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..." + # If we have a properly signed app, sign the DMG as well + if [[ "$SIGNING_RESULT" == "true" && "$DMG_CREATED" == "true" ]]; then + debug_log "Signing DMG with Developer ID certificate" + codesign --force --timestamp --sign "$SIGNING_IDENTITY" "$DMG_PATH" - # Create temporary folder for DMG contents - DMG_TMP_DIR=$(mktemp -d) - cp -R "${{ inputs.app-path }}" "$DMG_TMP_DIR/" - - # Add README with fancy formatting - cat > "$DMG_TMP_DIR/README.txt" << 'EOF' -# LuckyWorld Installation Instructions - -1. Drag the LuckyWorld application to your Applications folder -2. Double-click the "Install Helper" icon (optional - removes security warnings) -3. Enjoy! - -For technical support, contact: support@luckyrobots.com -EOF - - # Create a better helper script to remove quarantine attribute - echo "Creating helper script to remove quarantine attribute..." - mkdir -p "$DMG_TMP_DIR/scripts" - cat > "$DMG_TMP_DIR/scripts/InstallHelper.command" << 'EOF' -#!/bin/bash - -# Get the application name from the DMG -APP_NAME="LuckyWorld-Mac-Shipping.app" -APP_PATH="/Applications/$APP_NAME" - -echo "============================================" -echo "๐Ÿ” LuckyWorld Security Helper" -echo "============================================" - -if [ -d "$APP_PATH" ]; then - echo "โœ… Found $APP_NAME in Applications folder" - echo "๐Ÿงน Removing security attributes..." - xattr -dr com.apple.quarantine "$APP_PATH" - echo "โœ… Security attributes removed successfully!" - echo "๐Ÿš€ You can now launch the application normally" - osascript -e "display notification \"$APP_NAME is ready to use\" with title \"LuckyWorld Installation Complete\" subtitle \"Security attributes removed\"" -else - echo "โŒ $APP_NAME not found in Applications folder" - echo "โš ๏ธ Please drag the application to your Applications folder first" - osascript -e "display notification \"Please drag LuckyWorld to Applications folder first\" with title \"Installation Incomplete\" subtitle \"Application not found\"" - exit 1 -fi - -echo "============================================" -echo "Press any key to exit..." -read -n 1 -EOF - - # Make the script executable - chmod +x "$DMG_TMP_DIR/scripts/InstallHelper.command" - - # Create visually appealing backgrounds and icons - if [ -d "$DMG_TMP_DIR/scripts" ]; then - touch "$DMG_TMP_DIR/scripts/.keep" - fi - - # Try to use create-dmg if available, otherwise fall back to hdiutil - if command -v create-dmg &> /dev/null; then - echo "Using create-dmg for better DMG creation..." - - # Decide which keychain to use for getting identity - if [ "${{ steps.setup-cert.outputs.use_system_cert }}" = "true" ]; then - echo "Using system keychain identity" - IDENTITY_HASH=$(security find-identity -v -p codesigning | grep "Developer ID Application" | head -1 | awk '{print $2}') - else - security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" - IDENTITY_HASH=$(security find-identity -v -p codesigning "$KEYCHAIN_PATH" | grep "Developer ID Application" | head -1 | awk '{print $2}') - fi - - create-dmg \ - --volname "LuckyWorld Installer" \ - --codesign-identity "$IDENTITY_HASH" \ - --window-pos 200 120 \ - --window-size 800 400 \ - --icon-size 100 \ - --text-size 12 \ - --app-drop-link 600 170 \ - --hide-extension "$(basename "${{ inputs.app-path }}")" \ - --add-file "README.txt" 200 170 \ - --add-file "scripts/InstallHelper.command" 400 170 \ - --hide-extension "InstallHelper.command" \ - --no-internet-enable \ - "$DMG_FILE" \ - "$DMG_TMP_DIR" + if [ $? -eq 0 ]; then + debug_log "DMG signed successfully" else - # Fall back to hdiutil - hdiutil create -volname "LuckyWorld Installer" -srcfolder "$DMG_TMP_DIR" -ov -format UDZO "$DMG_FILE" + debug_log "DMG signing failed" fi - if [ -f "$DMG_FILE" ]; then - echo "โœ… Created DMG package: $DMG_FILE" + # If app was notarized, also notarize the DMG + if [[ "$NOTARIZATION_RESULT" == "true" ]]; then + debug_log "Notarizing DMG..." - # Sign the DMG with the same certificate used for the app - echo "Signing DMG file..." - - # 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 [ -n "$IDENTITY_HASH" ]; then - # Sign the DMG - codesign --force --sign "$IDENTITY_HASH" --options runtime --timestamp "$DMG_FILE" + # Notarize the DMG + if [[ "${{ inputs.notarization-method }}" == "api-key" ]]; then + debug_log "Notarizing DMG with API key method" - # Verify DMG signature - echo "Verifying DMG signature..." - codesign -vvv "$DMG_FILE" - - # Only notarize DMG if the app was successfully notarized - if [ "${{ steps.notarize.outputs.notarized }}" = "true" ]; then - echo "Notarizing DMG file..." + xcrun notarytool submit "$DMG_PATH" \ + --key "$API_KEY_FILE" \ + --key-id "${{ inputs.notary-api-key-id }}" \ + --issuer "${{ inputs.notary-api-key-issuer-id }}" \ + --wait > "$WORK_DIR/dmg_notarization_output.txt" 2>&1 - # 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 - # Use the same API key setup from the notarize step - echo "Using App Store Connect API key for DMG notarization..." - - echo "Submitting DMG for notarization..." - DMG_SUBMIT_OUTPUT=$(xcrun notarytool submit "$DMG_FILE" \ - --key ~/private_keys/AuthKey_${API_KEY_ID}.p8 \ - --key-id "$API_KEY_ID" \ - --issuer "$API_ISSUER_ID" --wait 2>&1) - - echo "DMG notarization submission output:" - echo "$DMG_SUBMIT_OUTPUT" - - # Extract DMG submission ID - DMG_SUBMISSION_ID=$(echo "$DMG_SUBMIT_OUTPUT" | grep -o "id: [a-f0-9\-]*" | head -1 | cut -d ' ' -f 2) - - if [ -n "$DMG_SUBMISSION_ID" ] && echo "$DMG_SUBMIT_OUTPUT" | grep -q "status: Accepted"; then - echo "โœ… DMG notarization completed successfully!" - - # Staple the DMG - echo "Stapling notarization ticket to DMG..." - xcrun stapler staple "$DMG_FILE" - - # Verify DMG stapling - echo "Verifying DMG stapling..." - xcrun stapler validate "$DMG_FILE" - - # Additional verification of DMG security - echo "Performing additional security verification of DMG..." - spctl --assess --verbose=4 --type open "$DMG_FILE" || echo "โš ๏ธ DMG security verification warning" - - echo "DMG is now fully signed, notarized, and stapled!" - else - echo "โš ๏ธ DMG notarization may have failed or is still in progress." - echo "The app itself is still properly notarized, but the DMG may need manual verification." - fi - elif [ "${{ inputs.notarization-method }}" = "app-password" ] && [ -n "$APPLE_ID" ] && [ -n "$APP_PASSWORD" ] && [ -n "$APPLE_TEAM_ID" ]; then - # Use App-specific password for DMG notarization - echo "Using App-specific password for DMG notarization..." - - echo "Submitting DMG for notarization..." - DMG_SUBMIT_OUTPUT=$(xcrun notarytool submit "$DMG_FILE" \ - --apple-id "$APPLE_ID" \ - --password "$APP_PASSWORD" \ - --team-id "$APPLE_TEAM_ID" --wait 2>&1) - - echo "DMG notarization submission output:" - echo "$DMG_SUBMIT_OUTPUT" - - # Extract DMG submission ID - DMG_SUBMISSION_ID=$(echo "$DMG_SUBMIT_OUTPUT" | grep -o "id: [a-f0-9\-]*" | head -1 | cut -d ' ' -f 2) - - if [ -n "$DMG_SUBMISSION_ID" ] && echo "$DMG_SUBMIT_OUTPUT" | grep -q "status: Accepted"; then - echo "โœ… DMG notarization completed successfully!" - - # Staple the DMG - echo "Stapling notarization ticket to DMG..." - xcrun stapler staple "$DMG_FILE" - - # Verify DMG stapling - echo "Verifying DMG stapling..." - xcrun stapler validate "$DMG_FILE" - - # Additional verification of DMG security - echo "Performing additional security verification of DMG..." - spctl --assess --verbose=4 --type open "$DMG_FILE" || echo "โš ๏ธ DMG security verification warning" - - echo "DMG is now fully signed, notarized, and stapled!" - else - echo "โš ๏ธ DMG notarization may have failed or is still in progress." - echo "The app itself is still properly notarized, but the DMG may need manual verification." - fi + cat "$WORK_DIR/dmg_notarization_output.txt" | tee -a "$DEBUG_LOG_PATH" + + DMG_REQUEST_STATUS=$(grep -o "status: .*" "$WORK_DIR/dmg_notarization_output.txt" | cut -d ' ' -f2) + + if [[ "$DMG_REQUEST_STATUS" == "Accepted" ]]; then + debug_log "DMG notarization successful" + + # Staple DMG + debug_log "Stapling notarization ticket to DMG" + xcrun stapler staple "$DMG_PATH" + if [ $? -eq 0 ]; then + debug_log "DMG stapling successful" else - echo "โš ๏ธ DMG not notarized due to missing credentials." - echo "The app itself is properly notarized, but the DMG is only signed." + debug_log "DMG stapling failed" fi else - echo "App was not notarized, skipping DMG notarization." + debug_log "DMG notarization failed or timed out" + cat "$WORK_DIR/dmg_notarization_output.txt" fi + else - echo "โš ๏ธ No valid identity found for DMG signing. DMG will be created but not signed." + debug_log "Notarizing DMG with app-specific password method" + + xcrun altool --notarize-app \ + --primary-bundle-id "$BUNDLE_ID.dmg" \ + --username "${{ inputs.apple-id }}" \ + --password "${{ inputs.app-password }}" \ + --file "$DMG_PATH" \ + > "$WORK_DIR/dmg_notarization_output.txt" 2>&1 + + cat "$WORK_DIR/dmg_notarization_output.txt" | tee -a "$DEBUG_LOG_PATH" + + DMG_REQUEST_UUID=$(grep -o "RequestUUID = .*" "$WORK_DIR/dmg_notarization_output.txt" | cut -d ' ' -f3) + + if [[ -n "$DMG_REQUEST_UUID" ]]; then + debug_log "DMG notarization request submitted, UUID: $DMG_REQUEST_UUID" + + # Wait for notarization to complete + debug_log "Waiting for DMG notarization to complete..." + TIMEOUT=10 # 10 minutes timeout for DMG + COUNT=0 + DMG_NOTARIZATION_STATUS="in progress" + + while [[ "$DMG_NOTARIZATION_STATUS" == "in progress" && $COUNT -lt $TIMEOUT ]]; do + sleep 60 + + xcrun altool --notarization-info "$DMG_REQUEST_UUID" \ + --username "${{ inputs.apple-id }}" \ + --password "${{ inputs.app-password }}" \ + > "$WORK_DIR/dmg_notarization_info.txt" 2>&1 + + cat "$WORK_DIR/dmg_notarization_info.txt" | tee -a "$DEBUG_LOG_PATH" + + DMG_NOTARIZATION_STATUS=$(grep -o "Status: .*" "$WORK_DIR/dmg_notarization_info.txt" | cut -d ':' -f2 | xargs) + + debug_log "DMG notarization status: $DMG_NOTARIZATION_STATUS" + COUNT=$((COUNT+1)) + done + + if [[ "$DMG_NOTARIZATION_STATUS" == "success" ]]; then + debug_log "DMG notarization successful" + + # Staple DMG + debug_log "Stapling notarization ticket to DMG" + xcrun stapler staple "$DMG_PATH" + if [ $? -eq 0 ]; then + debug_log "DMG stapling successful" + else + debug_log "DMG stapling failed" + fi + else + debug_log "DMG notarization failed or timed out: $DMG_NOTARIZATION_STATUS" + if [[ -f "$WORK_DIR/dmg_notarization_info.txt" ]]; then + cat "$WORK_DIR/dmg_notarization_info.txt" + fi + fi + else + debug_log "DMG notarization submission failed, no UUID returned" + cat "$WORK_DIR/dmg_notarization_output.txt" + fi fi - - # 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" + fi + fi + + # Final verification of all distribution artifacts + debug_log "Verifying final distribution artifacts" + + # Check ZIP file + if [[ -f "$ZIP_PATH" ]]; then + ZIP_SIZE=$(du -h "$ZIP_PATH" | cut -f1) + debug_log "ZIP package size: $ZIP_SIZE" + + # Verify ZIP integrity + unzip -t "$ZIP_PATH" > /dev/null + if [ $? -eq 0 ]; then + debug_log "ZIP package integrity verified" 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 - - # Final verification of all artifacts - echo "๐Ÿ” Final verification of all distribution artifacts..." - - if [ -f "$ZIP_FILE" ]; then - echo "Verifying ZIP package integrity..." - ditto -v -x "$ZIP_FILE" /tmp/verify_app_extraction || echo "โš ๏ธ ZIP extraction test failed" - rm -rf /tmp/verify_app_extraction - fi - - if [ -f "$DMG_FILE" ]; then - echo "Verifying DMG file signature..." - codesign -vvv "$DMG_FILE" || echo "โš ๏ธ DMG signature verification failed" - - # Check if DMG was notarized successfully - if [ "${{ steps.notarize.outputs.notarized }}" = "true" ]; then - echo "Verifying DMG stapling..." - xcrun stapler validate "$DMG_FILE" || echo "โš ๏ธ DMG stapling verification failed" + debug_log "ZIP package may be corrupted" + fi + fi + + # Check DMG file + if [[ -f "$DMG_PATH" ]]; then + DMG_SIZE=$(du -h "$DMG_PATH" | cut -f1) + debug_log "DMG package size: $DMG_SIZE" + + # Verify DMG signature if signed + if [[ "$SIGNING_RESULT" == "true" ]]; then + codesign -vvv "$DMG_PATH" 2>&1 | tee -a "$DEBUG_LOG_PATH" || debug_log "DMG signature verification failed" fi fi - - - name: Cleanup - if: always() shell: bash + + - name: Set outputs + id: set-outputs run: | - echo "๐Ÿงน Cleaning up..." - security delete-keychain "$KEYCHAIN_PATH" 2>/dev/null || true - rm -f *-notarize.zip || true - echo "โœ… Cleanup complete" \ No newline at end of file + debug_log "Setting action outputs" + + # Pass through environment variables to outputs + if [[ "$SIGNING_RESULT" == "true" ]]; then + echo "signed=true" >> $GITHUB_OUTPUT + elif [[ "$SIGNING_RESULT" == "ad-hoc" ]]; then + echo "signed=ad-hoc" >> $GITHUB_OUTPUT + else + echo "signed=none" >> $GITHUB_OUTPUT + fi + + if [[ "$NOTARIZATION_RESULT" == "true" ]]; then + echo "notarized=true" >> $GITHUB_OUTPUT + else + echo "notarized=false" >> $GITHUB_OUTPUT + fi + + echo "app-path=$APP_PATH" >> $GITHUB_OUTPUT + echo "zip-path=$ZIP_PATH" >> $GITHUB_OUTPUT + + if [[ "$DMG_CREATED" == "true" ]]; then + echo "package-path=$DMG_PATH" >> $GITHUB_OUTPUT + else + echo "package-path=$ZIP_PATH" >> $GITHUB_OUTPUT + fi + + debug_log "Action completed" + shell: bash + + - name: Clean up + if: always() + run: | + debug_log "Cleaning up" + + # Clean up keychain + if [[ -n "$KEYCHAIN_NAME" ]]; then + security delete-keychain "$KEYCHAIN_NAME" || true + debug_log "Keychain deleted" + fi + + # Clean up temporary files + if [[ -d "$WORK_DIR" ]]; then + rm -rf "$WORK_DIR" || true + debug_log "Temporary files deleted" + fi + + debug_log "Cleanup completed" + shell: bash \ No newline at end of file diff --git a/.gitea/workflows/test-macos-build.yml b/.gitea/workflows/test-macos-build.yml index 65af91e1..4c44d1fe 100644 --- a/.gitea/workflows/test-macos-build.yml +++ b/.gitea/workflows/test-macos-build.yml @@ -15,6 +15,13 @@ jobs: lfs: true fetch-depth: 0 + # Enable debug logging + - name: Enable Debug Logging + run: | + echo "ACTIONS_RUNNER_DEBUG=true" >> $GITHUB_ENV + echo "ACTIONS_STEP_DEBUG=true" >> $GITHUB_ENV + shell: bash + # Setup environment for build - name: Setup environment run: | @@ -34,6 +41,22 @@ jobs: echo "Environment setup complete" shell: bash + # Restore cache for build dependencies + - name: Restore Build Cache + id: build-cache + uses: actions/cache@v4 + with: + path: | + DerivedDataCache + Intermediate + Saved/Autosaves + Saved/Config + .unreal + key: ${{ runner.os }}-macbuild-${{ hashFiles('**/*.uproject') }}-${{ hashFiles('Config/**') }} + restore-keys: | + ${{ runner.os }}-macbuild-${{ hashFiles('**/*.uproject') }}- + ${{ runner.os }}-macbuild- + # Build for macOS - use your own build script - name: Build for macOS run: | @@ -144,7 +167,27 @@ jobs: echo "Found entitlements file: ${{ env.ENTITLEMENTS_FILE }}" fi shell: bash - + + # Save cache for next workflow run + - name: Save Build Cache + if: always() + uses: actions/cache/save@v4 + with: + path: | + DerivedDataCache + Intermediate + Saved/Autosaves + Saved/Config + .unreal + key: ${{ steps.build-cache.outputs.cache-primary-key }} + + # Create a debug log file for notarize action + - name: Create debug log directory + run: | + mkdir -p debug_logs + echo "DEBUG_LOG_PATH=$(pwd)/debug_logs/notarize_log.txt" >> $GITHUB_ENV + shell: bash + # Use the macos-notarize action to sign and notarize the app - name: Sign and Notarize macOS App uses: ./.gitea/actions/macos-notarize @@ -162,6 +205,15 @@ jobs: bundle-id: ${{ env.BUNDLE_ID }} fallback-to-adhoc: 'false' + # Upload debug logs if available + - name: Upload Debug Logs + uses: actions/upload-artifact@v3 + if: always() + with: + name: notarize-debug-logs + path: debug_logs + retention-days: 7 + # Upload only the DMG file as main distribution artifact - name: Upload Mac Distribution DMG uses: actions/upload-artifact@v3 @@ -169,7 +221,7 @@ jobs: with: name: LuckyWorld-Mac-Distribution path: ${{ steps.sign-and-notarize.outputs.package-path }} - retention-days: 30 + retention-days: 30 # Report results - name: Report Results