From 6c29eb93be09f8205b6c1ed4a8e46a95e863cfb1 Mon Sep 17 00:00:00 2001 From: Ozgur Ersoy Date: Wed, 16 Apr 2025 01:41:36 +0200 Subject: [PATCH] fix(actions): enhance macOS notarization workflow with detailed debug logging, improved variable management, and streamlined artifact handling --- .gitea/workflows/test-macos-build.yml | 525 ++++++++++++++++++++++++-- 1 file changed, 503 insertions(+), 22 deletions(-) diff --git a/.gitea/workflows/test-macos-build.yml b/.gitea/workflows/test-macos-build.yml index 4c44d1fe..65466571 100644 --- a/.gitea/workflows/test-macos-build.yml +++ b/.gitea/workflows/test-macos-build.yml @@ -188,22 +188,503 @@ jobs: 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 - id: sign-and-notarize - with: - app-path: ${{ env.APP_PATH }} - entitlements-file: ${{ env.ENTITLEMENTS_FILE }} - team-id: ${{ secrets.APPLE_TEAM_ID }} - certificate-base64: ${{ secrets.MACOS_CERTIFICATE }} - certificate-password: ${{ secrets.MACOS_CERTIFICATE_PWD }} - notarization-method: 'api-key' - notary-api-key-id: ${{ secrets.NOTARY_API_KEY_ID }} - notary-api-key-issuer-id: ${{ secrets.NOTARY_API_KEY_ISSUER_ID }} - notary-api-key-path: ${{ secrets.NOTARY_API_KEY_PATH }} - bundle-id: ${{ env.BUNDLE_ID }} - fallback-to-adhoc: 'false' + # Beginning of macos-notarize steps + - name: Setup debug environment + id: setup-debug-env + run: | + # 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 + + # 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: ${{ env.APP_PATH }}" + debug_log "Team ID: ${{ secrets.APPLE_TEAM_ID }}" + debug_log "Notarization method: api-key" + debug_log "Bundle ID: ${{ env.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 + + # Set paths + echo "APP_PATH=${{ env.APP_PATH }}" >> $GITHUB_ENV + + # Generate working directory for temp files + WORK_DIR="$(mktemp -d)" + echo "WORK_DIR=$WORK_DIR" >> $GITHUB_ENV + + # Set bundle id (from input or extract from app) + if [[ -n "${{ env.BUNDLE_ID }}" ]]; then + BUNDLE_ID="${{ env.BUNDLE_ID }}" + else + BUNDLE_ID=$(/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "${{ env.APP_PATH }}/Contents/Info.plist") + fi + echo "BUNDLE_ID=$BUNDLE_ID" >> $GITHUB_ENV + + # Get app name from bundle path + APP_NAME=$(basename "${{ env.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 + echo "Using API key method for notarization" + + # Create API key file + API_KEY_FILE="$WORK_DIR/api_key.p8" + echo "${{ secrets.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: ${{ secrets.NOTARY_API_KEY_ID }}" + debug_log "API key issuer ID: ${{ secrets.NOTARY_API_KEY_ISSUER_ID }}" + 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 "${{ secrets.MACOS_CERTIFICATE }}" | base64 --decode > "$CERTIFICATE_PATH" + + # Add to keychain + debug_log "Importing certificate into keychain" + security import "$CERTIFICATE_PATH" -k "$KEYCHAIN_NAME" -P "${{ secrets.MACOS_CERTIFICATE_PWD }}" -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: ${{ secrets.APPLE_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 [[ "false" == "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 + + # Make sure entitlements file is defined and reset variable name + ENTITLEMENTS_PATH="${{ env.ENTITLEMENTS_FILE }}" + + # Sign the app + if [[ "$CERTIFICATE_AVAILABLE" == "true" ]]; then + debug_log "Signing with Developer ID certificate" + + # 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 + 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 + debug_log "Unexpected certificate state. Skipping signing." + echo "SIGNING_RESULT=none" >> $GITHUB_ENV + fi + + # Verify signing + debug_log "Verifying app signature..." + codesign -dvv "$APP_PATH" + shell: bash + + - name: Verify notarization and stapling + id: verify-notarization + if: env.SIGNING_RESULT == 'true' + run: | + debug_log "Verifying app signature and code requirements before notarization" + + # 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 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 using API key method + debug_log "Notarizing with API key method" + + # Submit for notarization + debug_log "Submitting app for notarization..." + xcrun notarytool submit "$ZIP_PATH" \ + --key "$API_KEY_FILE" \ + --key-id "${{ secrets.NOTARY_API_KEY_ID }}" \ + --issuer "${{ secrets.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) + + if [[ "$REQUEST_STATUS" == "Accepted" ]]; then + debug_log "Notarization successful" + echo "NOTARIZATION_RESULT=true" >> $GITHUB_ENV + else + debug_log "Notarization failed or timed out" + cat "$WORK_DIR/notarization_output.txt" + echo "NOTARIZATION_RESULT=false" >> $GITHUB_ENV + fi + shell: bash + + - name: Staple notarization ticket + id: staple-ticket + if: env.SIGNING_RESULT == 'true' && env.NOTARIZATION_RESULT == 'true' + run: | + debug_log "Stapling notarization ticket to app" + + # Staple the ticket + xcrun stapler staple "$APP_PATH" + STAPLE_RESULT=$? + + 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 + 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 + + # 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 + + # 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" + + if [ $? -eq 0 ]; then + debug_log "DMG signed successfully" + else + debug_log "DMG signing failed" + fi + + # If app was notarized, also notarize the DMG + if [[ "$NOTARIZATION_RESULT" == "true" ]]; then + debug_log "Notarizing DMG..." + + # Notarize the DMG using API key method + debug_log "Notarizing DMG with API key method" + + xcrun notarytool submit "$DMG_PATH" \ + --key "$API_KEY_FILE" \ + --key-id "${{ secrets.NOTARY_API_KEY_ID }}" \ + --issuer "${{ secrets.NOTARY_API_KEY_ISSUER_ID }}" \ + --wait > "$WORK_DIR/dmg_notarization_output.txt" 2>&1 + + 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 + debug_log "DMG stapling failed" + fi + else + debug_log "DMG notarization failed or timed out" + cat "$WORK_DIR/dmg_notarization_output.txt" + fi + 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 + 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 + + # Set output variables + if [[ "$SIGNING_RESULT" == "true" ]]; then + echo "SIGNED_STATUS=true" >> $GITHUB_ENV + elif [[ "$SIGNING_RESULT" == "ad-hoc" ]]; then + echo "SIGNED_STATUS=ad-hoc" >> $GITHUB_ENV + else + echo "SIGNED_STATUS=none" >> $GITHUB_ENV + fi + + if [[ "$NOTARIZATION_RESULT" == "true" ]]; then + echo "NOTARIZED_STATUS=true" >> $GITHUB_ENV + else + echo "NOTARIZED_STATUS=false" >> $GITHUB_ENV + fi + + if [[ "$DMG_CREATED" == "true" ]]; then + echo "PACKAGE_PATH=$DMG_PATH" >> $GITHUB_ENV + else + echo "PACKAGE_PATH=$ZIP_PATH" >> $GITHUB_ENV + fi + 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 # Upload debug logs if available - name: Upload Debug Logs @@ -217,21 +698,21 @@ jobs: # Upload only the DMG file as main distribution artifact - name: Upload Mac Distribution DMG uses: actions/upload-artifact@v3 - if: steps.sign-and-notarize.outputs.notarized == 'true' && steps.sign-and-notarize.outputs.signed != 'none' + if: env.NOTARIZED_STATUS == 'true' && env.SIGNED_STATUS != 'none' with: name: LuckyWorld-Mac-Distribution - path: ${{ steps.sign-and-notarize.outputs.package-path }} + path: ${{ env.PACKAGE_PATH }} retention-days: 30 # Report results - name: Report Results run: | - echo "🔐 App signing: ${{ steps.sign-and-notarize.outputs.signed }}" - echo "🔏 App notarization: ${{ steps.sign-and-notarize.outputs.notarized }}" + echo "🔐 App signing: ${{ env.SIGNED_STATUS }}" + echo "🔏 App notarization: ${{ env.NOTARIZED_STATUS }}" - if [ "${{ steps.sign-and-notarize.outputs.signed }}" != "none" ]; then + if [ "${{ env.SIGNED_STATUS }}" != "none" ]; then echo "✅ Packaging completed successfully!" - echo "Final package: ${{ steps.sign-and-notarize.outputs.package-path }}" + echo "Final package: ${{ env.PACKAGE_PATH }}" else echo "⚠️ App was not signed - check the logs for details" fi