name: macOS Build, Sign and Notarize on: workflow_dispatch: # Manuel tetikleme push: branches: [ozgur/build] jobs: build-sign-notarize: runs-on: macos steps: - name: Checkout repository uses: actions/checkout@v3 with: lfs: true fetch-depth: 0 - name: Setup environment run: | # Çalışma dizini yolunu al WORKSPACE_DIR="$(pwd)" echo "WORKSPACE_DIR=$WORKSPACE_DIR" >> "$GITHUB_ENV" echo "ENTITLEMENTS_FILE=LuckyWorld.entitlements" >> "$GITHUB_ENV" # CI ortam değişkenini true olarak ayarla echo "CI=true" >> "$GITHUB_ENV" # Build için gerekli dizinleri oluştur mkdir -p Builds/Mac mkdir -p PackagedReleases mkdir -p ArchivedApps echo "Environment setup complete" shell: bash - name: Setup Certificate and Keychain id: setup-cert shell: bash env: CERTIFICATE_BASE64: ${{ secrets.MACOS_CERTIFICATE }} CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PWD }} APPLE_TEAM_ID: ${{ secrets.APPLE_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: Build for macOS id: build run: | if [ -f "./scripts/mac_build.sh" ]; then chmod +x ./scripts/mac_build.sh # Set CI environment variable explicitly before running export CI=true ./scripts/mac_build.sh # Check if the build succeeded by looking for the app APP_PATHS=$(find ./Builds -type d -name "*.app" 2>/dev/null || echo "") if [ -z "$APP_PATHS" ]; then APP_PATHS=$(find ./Saved/StagedBuilds -type d -name "*.app" 2>/dev/null || echo "") fi if [ -z "$APP_PATHS" ]; then echo "❌ ERROR: Build command appeared to succeed but no app bundle was found!" echo "This usually means the build failed but didn't properly return an error code." exit 1 fi else echo "ERROR: Build script not found at ./scripts/mac_build.sh" exit 1 fi shell: bash - name: Find app bundle id: find-app run: | # Add error handling set +e # Don't exit immediately on error for this block echo "Build status check..." if [ ! -d "./Builds" ] && [ ! -d "./Saved/StagedBuilds" ]; then echo "❌ ERROR: Build directories do not exist. Build likely failed." exit 1 fi # First check Saved/StagedBuilds directory - where Unreal often places built apps echo "Checking Saved/StagedBuilds directory..." APP_PATHS=$(find ./Saved/StagedBuilds -type d -name "*.app" 2>/dev/null || echo "") # If not found, check Builds directory if [ -z "$APP_PATHS" ]; then echo "No app found in Saved/StagedBuilds, checking Builds directory..." APP_PATHS=$(find ./Builds -type d -name "*.app" 2>/dev/null || echo "") fi # If still not found, check the whole workspace if [ -z "$APP_PATHS" ]; then echo "No app found in Builds, checking entire workspace..." APP_PATHS=$(find . -type d -name "*.app" -not -path "*/\.*" 2>/dev/null || echo "") fi if [ -z "$APP_PATHS" ]; then echo "❌ ERROR: Could not find any app bundles!" echo "Listing all directories to help debug:" find . -type d -maxdepth 3 | sort exit 1 fi echo "Found potential app bundles:" echo "$APP_PATHS" # Use the first app path found (preferably the main app, not a child app) MAIN_APP_PATH=$(echo "$APP_PATHS" | grep -v "CrashReportClient" | head -1 || echo "$APP_PATHS" | head -1) echo "Using app bundle: $MAIN_APP_PATH" # Make sure app exists - using local variable if [ ! -d "$MAIN_APP_PATH" ]; then echo "❌ ERROR: App bundle not found at $MAIN_APP_PATH!" exit 1 fi # Export APP_PATH for next steps to use echo "APP_PATH=$MAIN_APP_PATH" >> "$GITHUB_ENV" # Get bundle ID from Info.plist for reference (not modifying) if [ -f "$MAIN_APP_PATH/Contents/Info.plist" ]; then BUNDLE_ID=$(/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "$MAIN_APP_PATH/Contents/Info.plist") echo "Detected bundle ID: $BUNDLE_ID" echo "BUNDLE_ID=$BUNDLE_ID" >> "$GITHUB_ENV" fi shell: bash - name: Sign App id: sign shell: bash run: | echo "🔏 Signing app with Developer ID certificate..." # Check if app path exists if [ ! -d "${{ env.APP_PATH }}" ]; then echo "❌ App bundle not found at ${{ env.APP_PATH }}" echo "::set-output name=signed::none" exit 1 fi # Check if entitlements file exists if [ ! -f "${{ env.ENTITLEMENTS_FILE }}" ]; then echo "❌ Entitlements file not found at ${{ env.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" echo "Falling back to ad-hoc signing for testing..." # Use ad-hoc identity as fallback codesign --force --deep --verbose --options runtime --entitlements "${{ env.ENTITLEMENTS_FILE }}" --sign - --timestamp "${{ env.APP_PATH }}" echo "::set-output name=signed::adhoc" 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 "${{ env.APP_PATH }}" -name "*.dylib" | while read -r dylib; do echo "Signing: $dylib" codesign --force --verbose --options runtime --entitlements "${{ env.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 "${{ env.APP_PATH }}" -name "*.so" | while read -r so; do echo "Signing: $so" codesign --force --verbose --options runtime --entitlements "${{ env.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 "${{ env.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 "${{ env.ENTITLEMENTS_FILE }}" --sign "$IDENTITY_HASH" --timestamp "$exe" || echo "⚠️ Failed to sign: $exe" done # Sign all frameworks echo "Signing frameworks..." find "${{ env.APP_PATH }}" -path "*.framework" -type d | while read -r framework; do echo "Signing framework: $framework" codesign --force --verbose --options runtime --entitlements "${{ env.ENTITLEMENTS_FILE }}" --sign "$IDENTITY_HASH" --timestamp "$framework" || echo "⚠️ Failed to sign: $framework" done # Final signing of the main bundle echo "🔐 Performing final signing of the main app bundle..." codesign --force --deep --verbose --options runtime --entitlements "${{ env.ENTITLEMENTS_FILE }}" --sign "$IDENTITY_HASH" --timestamp "${{ env.APP_PATH }}" echo "::set-output name=signed::identity" fi # Verify signing echo "🔍 Verifying signature..." codesign -vvv --deep --strict "${{ env.APP_PATH }}" # Check entitlements echo "🔍 Checking entitlements..." codesign -d --entitlements - "${{ env.APP_PATH }}" - name: Notarize App id: notarize if: steps.sign.outputs.signed != 'none' shell: bash env: API_KEY_ID: ${{ secrets.NOTARY_API_KEY_ID }} API_ISSUER_ID: ${{ secrets.NOTARY_API_KEY_ISSUER_ID }} API_KEY_PATH: ${{ secrets.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 "${{ env.APP_PATH }}" .app) BUNDLE_ID="${{ env.BUNDLE_ID }}" # If bundle ID is not provided, try to extract from Info.plist if [ -z "$BUNDLE_ID" ]; then if [ -f "${{ env.APP_PATH }}/Contents/Info.plist" ]; then BUNDLE_ID=$(/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "${{ env.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 [ -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 "${{ env.APP_PATH }}" "$ZIP_PATH" echo "Submitting for notarization with API key..." # First, submit without waiting for completion SUBMIT_OUTPUT=$(xcrun notarytool submit "$ZIP_PATH" \ --key ~/private_keys/AuthKey_${API_KEY_ID}.p8 \ --key-id "$API_KEY_ID" \ --issuer "$API_ISSUER_ID" 2>&1) SUBMIT_STATUS=$? # Display output for debugging echo "Notarization submission output:" echo "$SUBMIT_OUTPUT" echo "Submission exit status: $SUBMIT_STATUS" # Check if submission was successful if [ $SUBMIT_STATUS -ne 0 ]; then echo "❌ Failed to submit for notarization. Exit code: $SUBMIT_STATUS" exit 1 fi # Extract submission ID for log retrieval SUBMISSION_ID=$(echo "$SUBMIT_OUTPUT" | grep -o "id: [a-f0-9\-]*" | head -1 | cut -d ' ' -f 2) if [ -z "$SUBMISSION_ID" ]; then echo "❌ Could not extract submission ID from output. Notarization failed." exit 1 fi echo "Submission ID: $SUBMISSION_ID" echo "Waiting for notarization to complete..." # Now wait for the processing to complete COMPLETE=false MAX_ATTEMPTS=60 # Maximum number of attempts (60 * 30 seconds = 30 minutes max) ATTEMPT=1 while [ "$COMPLETE" = "false" ] && [ $ATTEMPT -le $MAX_ATTEMPTS ]; do echo "Checking notarization status (attempt $ATTEMPT of $MAX_ATTEMPTS)..." INFO_OUTPUT=$(xcrun notarytool info "$SUBMISSION_ID" \ --key ~/private_keys/AuthKey_${API_KEY_ID}.p8 \ --key-id "$API_KEY_ID" \ --issuer "$API_ISSUER_ID" 2>&1) INFO_STATUS=$? echo "Status check output:" echo "$INFO_OUTPUT" # Check if the notarization is complete if echo "$INFO_OUTPUT" | grep -q "status: Accepted"; then echo "✅ Notarization completed successfully!" COMPLETE=true FINAL_STATUS="Accepted" elif echo "$INFO_OUTPUT" | grep -q "status: Invalid"; then echo "❌ Notarization failed with status: Invalid" COMPLETE=true FINAL_STATUS="Invalid" elif echo "$INFO_OUTPUT" | grep -q "status: Rejected"; then echo "❌ Notarization failed with status: Rejected" COMPLETE=true FINAL_STATUS="Rejected" else echo "Notarization still in progress. Waiting 30 seconds before checking again..." sleep 30 ATTEMPT=$((ATTEMPT + 1)) fi done # Handle timeout if [ "$COMPLETE" = "false" ]; then echo "❌ Notarization timed out after $MAX_ATTEMPTS attempts." exit 1 fi # Handle completed notarization if [ "$FINAL_STATUS" = "Accepted" ]; then # Get logs for information (even though successful) echo "📋 Getting notarization logs for information..." LOGS_OUTPUT=$(xcrun notarytool log "$SUBMISSION_ID" \ --key ~/private_keys/AuthKey_${API_KEY_ID}.p8 \ --key-id "$API_KEY_ID" \ --issuer "$API_ISSUER_ID" 2>&1) echo "==== NOTARIZATION LOG SUMMARY ====" echo "$LOGS_OUTPUT" | head -20 echo "==================================" # Staple the notarization ticket echo "Stapling notarization ticket..." xcrun stapler staple -v "${{ env.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 "${{ env.APP_PATH }}" echo "::set-output name=notarized::true" else echo "⚠️ Stapling completed with status $STAPLE_STATUS (may still be valid)" fi 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 "==================================" echo "❌ Notarization failed with status: $FINAL_STATUS" exit 1 fi # Clean up rm -rf ~/private_keys else echo "⚠️ Missing notarization credentials. Skipping notarization." echo "Set these secrets for notarization:" echo " - NOTARY_API_KEY_ID: Your API key ID" echo " - NOTARY_API_KEY_ISSUER_ID: Your API issuer ID" echo " - NOTARY_API_KEY_PATH: Your p8 file content" fi - name: Create DMG Package id: package if: steps.notarize.outputs.notarized == 'true' shell: bash run: | echo "📦 Creating DMG package..." # Get app name for DMG file naming APP_NAME=$(basename "${{ env.APP_PATH }}" .app) # Create a DMG package DMG_FILE="./PackagedReleases/${APP_NAME}-Signed-Notarized.dmg" rm -f "$DMG_FILE" 2>/dev/null || true # Create temporary folder for DMG contents DMG_TMP_DIR=$(mktemp -d) cp -R "${{ env.APP_PATH }}" "$DMG_TMP_DIR/" # Create DMG file with the app hdiutil create -volname "${APP_NAME}" -srcfolder "$DMG_TMP_DIR" -ov -format UDZO "$DMG_FILE" if [ -f "$DMG_FILE" ]; then echo "✅ Created DMG package: $DMG_FILE" echo "Size: $(du -h "$DMG_FILE" | cut -f1)" echo "DMG_PATH=$DMG_FILE" >> $GITHUB_ENV else echo "❌ Failed to create DMG package" exit 1 fi # Clean up temporary directory rm -rf "$DMG_TMP_DIR" - name: Upload DMG Package uses: actions/upload-artifact@v3 if: steps.package.outputs.DMG_PATH != '' with: name: LuckyWorld-macOS-Signed-Notarized path: ${{ env.DMG_PATH }} retention-days: 30 - 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"