From 336f57f6f02ea10a91b2c9138406c77572ddf7a7 Mon Sep 17 00:00:00 2001 From: Ozgur Ersoy Date: Mon, 14 Apr 2025 16:04:41 +0200 Subject: [PATCH] feat(actions): add macOS Sign and Notarize action for automated app signing and notarization --- .gitea/actions/macos-notarize/action.yml | 346 +++++++++++++++++++++++ 1 file changed, 346 insertions(+) create mode 100644 .gitea/actions/macos-notarize/action.yml diff --git a/.gitea/actions/macos-notarize/action.yml b/.gitea/actions/macos-notarize/action.yml new file mode 100644 index 00000000..554319df --- /dev/null +++ b/.gitea/actions/macos-notarize/action.yml @@ -0,0 +1,346 @@ +name: "macOS Sign and Notarize" +description: "Signs and notarizes macOS applications with Developer ID certificate" +author: moersoy" + +inputs: + app-path: + description: "Path to the app bundle (.app)" + required: true + entitlements-file: + description: "Path to the entitlements file (.entitlements)" + required: true + team-id: + description: "Apple Developer Team ID" + required: true + certificate-base64: + description: "Base64-encoded Developer ID Application certificate (.p12)" + required: true + certificate-password: + description: "Password for the Developer ID Application certificate" + required: true + notarization-method: + description: "Method to use for notarization: 'api-key' or 'app-password'" + required: false + default: "api-key" + notary-user: + description: "Apple ID for notarization (for app-password method)" + required: false + notary-password: + description: "App-specific password for notarization (for app-password method)" + required: false + notary-api-key-id: + description: "API Key ID for notarization (for api-key method)" + required: false + notary-api-key-issuer-id: + description: "API Issuer ID for notarization (for api-key method)" + required: false + notary-api-key-path: + description: "Path to or content of the API Key .p8 file (for api-key method)" + required: false + bundle-id: + description: "App bundle identifier (com.example.app)" + required: false + fallback-to-adhoc: + description: "Whether to fall back to ad-hoc signing if certificate is invalid" + required: false + default: "true" + +outputs: + signed: + description: "Whether the app was signed (identity, adhoc, or none)" + value: ${{ steps.sign.outputs.signed }} + notarized: + description: "Whether the app was notarized (true or false)" + value: ${{ steps.notarize.outputs.notarized }} + package-path: + description: "Path to the final package" + value: ${{ steps.package.outputs.package-path }} + +runs: + using: "composite" + steps: + - name: Setup Certificate + id: setup-cert + shell: bash + env: + CERTIFICATE_BASE64: ${{ inputs.certificate-base64 }} + CERTIFICATE_PASSWORD: ${{ inputs.certificate-password }} + APPLE_TEAM_ID: ${{ inputs.team-id }} + run: | + echo "๐Ÿ” Setting up certificate..." + + # Create a temporary directory for certificates + CERT_DIR="$HOME/certificates" + mkdir -p "$CERT_DIR" + + # Decode the certificate to a p12 file + echo "$CERTIFICATE_BASE64" | base64 --decode > "$CERT_DIR/certificate.p12" + + # Create keychain + KEYCHAIN_PATH="$CERT_DIR/app-signing.keychain-db" + KEYCHAIN_PASSWORD="temppassword123" + + # Delete existing keychain if it exists + security delete-keychain "$KEYCHAIN_PATH" 2>/dev/null || true + + # Create new keychain + security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + security set-keychain-settings -t 3600 -u -l "$KEYCHAIN_PATH" + security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + + # Add to search list and make default + security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | tr -d '"') + security default-keychain -s "$KEYCHAIN_PATH" + + # Import certificate + echo "๐Ÿ”‘ Importing developer certificate..." + security import "$CERT_DIR/certificate.p12" -k "$KEYCHAIN_PATH" -P "$CERTIFICATE_PASSWORD" -T /usr/bin/codesign + + # Try with additional parameters if needed + security import "$CERT_DIR/certificate.p12" -k "$KEYCHAIN_PATH" -P "$CERTIFICATE_PASSWORD" -T /usr/bin/codesign -f pkcs12 || true + + # Set partition list for codesign to access keychain + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + + # Verify certificate + echo "๐Ÿ” Verifying code signing identities..." + security find-identity -v -p codesigning "$KEYCHAIN_PATH" + + # Try to use the System keychain as a fallback + echo "๐Ÿ” Checking system keychain for code signing identities..." + SYSTEM_IDENTITIES=$(security find-identity -v -p codesigning) + echo "$SYSTEM_IDENTITIES" + + if echo "$SYSTEM_IDENTITIES" | grep -q "Developer ID Application"; then + echo "โœ… Found Developer ID Application certificate in system keychain" + echo "::set-output name=use_system_cert::true" + else + echo "::set-output name=use_system_cert::false" + fi + + # Store keychain variables for later steps + echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> $GITHUB_ENV + echo "KEYCHAIN_PASSWORD=$KEYCHAIN_PASSWORD" >> $GITHUB_ENV + echo "APPLE_TEAM_ID=$APPLE_TEAM_ID" >> $GITHUB_ENV + + # Clean up + rm -f "$CERT_DIR/certificate.p12" + + - name: Sign App + id: sign + shell: bash + run: | + echo "๐Ÿ” Signing app with Developer ID certificate..." + + # Check if app path exists + if [ ! -d "${{ inputs.app-path }}" ]; then + echo "โŒ App bundle not found at ${{ inputs.app-path }}" + echo "::set-output name=signed::none" + exit 1 + fi + + # Check if entitlements file exists + if [ ! -f "${{ inputs.entitlements-file }}" ]; then + echo "โŒ Entitlements file not found at ${{ inputs.entitlements-file }}" + echo "::set-output name=signed::none" + exit 1 + fi + + # Decide which keychain to use + if [ "${{ steps.setup-cert.outputs.use_system_cert }}" = "true" ]; then + echo "Using system keychain identity" + # Get certificate hash instead of name to avoid ambiguity + IDENTITY_HASH=$(security find-identity -v -p codesigning | grep "Developer ID Application" | head -1 | awk '{print $2}') + echo "Using certificate hash: $IDENTITY_HASH" + else + # Make sure keychain is unlocked + security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + echo "Using custom keychain identity" + # Get certificate hash instead of name to avoid ambiguity + IDENTITY_HASH=$(security find-identity -v -p codesigning "$KEYCHAIN_PATH" | grep "Developer ID Application" | head -1 | awk '{print $2}') + echo "Using certificate hash: $IDENTITY_HASH" + fi + + if [ -z "$IDENTITY_HASH" ]; then + echo "โŒ No valid Developer ID Application certificate found" + + if [ "${{ inputs.fallback-to-adhoc }}" = "true" ]; then + echo "Falling back to ad-hoc signing for testing..." + # Use ad-hoc identity as fallback + codesign --force --deep --verbose --options runtime --entitlements "${{ inputs.entitlements-file }}" --sign - --timestamp "${{ inputs.app-path }}" + echo "::set-output name=signed::adhoc" + else + echo "Skipping signing. Set fallback-to-adhoc=true to use ad-hoc signing instead." + echo "::set-output name=signed::none" + exit 1 + fi + else + echo "Signing app bundle with Developer ID hash: $IDENTITY_HASH" + + # Sign the app bundle using the hash + codesign --force --deep --verbose --options runtime --entitlements "${{ inputs.entitlements-file }}" --sign "$IDENTITY_HASH" --timestamp "${{ inputs.app-path }}" + echo "::set-output name=signed::identity" + fi + + # Verify signing + echo "๐Ÿ” Verifying signature..." + codesign -vvv --deep --strict "${{ inputs.app-path }}" + + # Check entitlements + echo "๐Ÿ” Checking entitlements..." + codesign -d --entitlements - "${{ inputs.app-path }}" + + - name: Notarize App + id: notarize + if: steps.sign.outputs.signed != 'none' + shell: bash + env: + APPLE_ID: ${{ inputs.notary-user }} + APP_PASSWORD: ${{ inputs.notary-password }} + API_KEY_ID: ${{ inputs.notary-api-key-id }} + API_ISSUER_ID: ${{ inputs.notary-api-key-issuer-id }} + API_KEY_PATH: ${{ inputs.notary-api-key-path }} + run: | + echo "๐Ÿ“ค Notarizing app..." + + # Set default output + echo "::set-output name=notarized::false" + + # Get app name for zip file naming + APP_NAME=$(basename "${{ inputs.app-path }}" .app) + BUNDLE_ID="${{ inputs.bundle-id }}" + + # If bundle ID is not provided, try to extract from Info.plist + if [ -z "$BUNDLE_ID" ]; then + if [ -f "${{ inputs.app-path }}/Contents/Info.plist" ]; then + BUNDLE_ID=$(/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "${{ inputs.app-path }}/Contents/Info.plist") + echo "Extracted bundle ID: $BUNDLE_ID" + else + BUNDLE_ID="com.luckyrobots.app" + echo "Using default bundle ID: $BUNDLE_ID" + fi + fi + + # Check if we're using API key notarization method + if [ "${{ inputs.notarization-method }}" = "api-key" ] && [ -n "$API_KEY_ID" ] && [ -n "$API_ISSUER_ID" ] && [ -n "$API_KEY_PATH" ]; then + echo "Using App Store Connect API key for notarization..." + + # Create directory for API key if API_KEY_PATH contains content + mkdir -p ~/private_keys + + # Check if API_KEY_PATH is a path or content + if [[ "$API_KEY_PATH" == /* ]] && [ -f "$API_KEY_PATH" ]; then + # It's a path to a file + echo "Using API key from path: $API_KEY_PATH" + cp "$API_KEY_PATH" ~/private_keys/AuthKey_${API_KEY_ID}.p8 + else + # It contains the key content + echo "Using API key from content" + echo "$API_KEY_PATH" > ~/private_keys/AuthKey_${API_KEY_ID}.p8 + fi + + # Create zip for notarization + ZIP_PATH="${APP_NAME}-notarize.zip" + ditto -c -k --keepParent "${{ inputs.app-path }}" "$ZIP_PATH" + + echo "Submitting for notarization with API key..." + xcrun notarytool submit "$ZIP_PATH" \ + --key ~/private_keys/AuthKey_${API_KEY_ID}.p8 \ + --key-id "$API_KEY_ID" \ + --issuer "$API_ISSUER_ID" \ + --wait + + # Check if notarization succeeded + if [ $? -eq 0 ]; then + # Staple the notarization ticket + echo "Stapling notarization ticket..." + xcrun stapler staple "${{ inputs.app-path }}" + + # Verify notarization + echo "๐Ÿ” Verifying notarization..." + spctl --assess --verbose --type exec "${{ inputs.app-path }}" + + echo "::set-output name=notarized::true" + else + echo "โŒ Notarization failed" + 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..." + xcrun notarytool submit "$ZIP_PATH" \ + --apple-id "$APPLE_ID" \ + --password "$APP_PASSWORD" \ + --team-id "$APPLE_TEAM_ID" \ + --wait + + # Check if notarization succeeded + if [ $? -eq 0 ]; then + # Staple the notarization ticket + echo "Stapling notarization ticket..." + xcrun stapler staple "${{ inputs.app-path }}" + + # Verify notarization + echo "๐Ÿ” Verifying notarization..." + spctl --assess --verbose --type exec "${{ inputs.app-path }}" + + echo "::set-output name=notarized::true" + else + echo "โŒ Notarization failed" + fi + else + echo "โš ๏ธ Missing notarization credentials. Skipping notarization." + echo "For App Store Connect API key method, set these inputs:" + echo " - notarization-method: api-key" + echo " - notary-api-key-id: Your API key ID" + echo " - notary-api-key-issuer-id: Your API issuer ID" + echo " - notary-api-key-path: Path to or content of your p8 file" + echo "" + echo "For App-specific password method, set these inputs:" + echo " - notarization-method: app-password" + echo " - notary-user: Your Apple ID (email)" + echo " - notary-password: Your app-specific password" + echo " - team-id: Your Apple Developer team ID" + fi + + - name: Package App + id: package + if: steps.sign.outputs.signed != 'none' + shell: bash + run: | + echo "๐Ÿ“ฆ Packaging signed app..." + + # Get app name for zip file naming + APP_NAME=$(basename "${{ inputs.app-path }}" .app) + + if [ "${{ steps.notarize.outputs.notarized }}" = "true" ]; then + ZIP_FILE="${APP_NAME}-Signed-Notarized.zip" + echo "Creating distribution package with notarized app..." + else + ZIP_FILE="${APP_NAME}-Signed.zip" + echo "Creating distribution package with signed app..." + fi + + # Create zip package + ditto -c -k --keepParent "${{ inputs.app-path }}" "$ZIP_FILE" + + echo "โœ… Created package: $ZIP_FILE" + echo "::set-output name=package-path::$ZIP_FILE" + + - name: Cleanup + if: always() + shell: bash + run: | + echo "๐Ÿงน Cleaning up..." + security delete-keychain "$KEYCHAIN_PATH" 2>/dev/null || true + rm -f *-notarize.zip || true + echo "โœ… Cleanup complete" \ No newline at end of file