Build and Notarize SwiftFormat for Xcode #42
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build and Notarize SwiftFormat for Xcode | |
| on: | |
| release: | |
| types: [published] | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: 'Release tag (e.g. v1.2.3)' | |
| required: true | |
| jobs: | |
| build-and-notarize: | |
| name: Build and Notarize SwiftFormat for Xcode | |
| runs-on: macos-15 | |
| steps: | |
| - name: Select Xcode version | |
| uses: maxim-lobanov/setup-xcode@v1 | |
| with: | |
| xcode-version: '16.3' | |
| - name: Checkout | |
| uses: actions/checkout@v5 | |
| with: | |
| ref: ${{ github.event.release.tag_name || inputs.tag }} | |
| - name: Import Code Signing Certificate | |
| run: | | |
| # Debug: Check if secrets exist (without revealing them) | |
| if [ -z "${{ secrets.DEVELOPER_ID_CERTIFICATE_BASE64 }}" ]; then | |
| echo "ERROR: DEVELOPER_ID_CERTIFICATE_BASE64 secret is empty or not set" | |
| exit 1 | |
| fi | |
| if [ -z "${{ secrets.DEVELOPER_ID_CERTIFICATE_PASSWORD }}" ]; then | |
| echo "ERROR: DEVELOPER_ID_CERTIFICATE_PASSWORD secret is empty or not set" | |
| exit 1 | |
| fi | |
| echo "Secrets are present, proceeding with certificate import..." | |
| # Create keychain | |
| security create-keychain -p "" build.keychain | |
| security default-keychain -s build.keychain | |
| security unlock-keychain -p "" build.keychain | |
| security set-keychain-settings -t 3600 -l build.keychain | |
| # Import certificate | |
| echo "${{ secrets.DEVELOPER_ID_CERTIFICATE_BASE64 }}" | base64 --decode > certificate.p12 | |
| security import certificate.p12 -k build.keychain -P "${{ secrets.DEVELOPER_ID_CERTIFICATE_PASSWORD }}" -T /usr/bin/codesign | |
| security set-key-partition-list -S apple-tool:,apple: -s -k "" build.keychain | |
| # Clean up | |
| rm certificate.p12 | |
| - name: Archive SwiftFormat for Xcode App | |
| run: | | |
| ARCHIVE_PATH="build/SwiftFormatForXcode.xcarchive" | |
| xcodebuild \ | |
| -project SwiftFormat.xcodeproj \ | |
| -scheme "SwiftFormat for Xcode" \ | |
| -configuration Release \ | |
| -archivePath "$ARCHIVE_PATH" \ | |
| CODE_SIGN_STYLE=Manual \ | |
| CODE_SIGN_IDENTITY="Developer ID Application" \ | |
| DEVELOPMENT_TEAM="8VQKF583ED" \ | |
| PROVISIONING_PROFILE_SPECIFIER="" \ | |
| archive | |
| - name: Copy and Sign App from Archive | |
| id: export-app | |
| run: | | |
| ARCHIVE_PATH="build/SwiftFormatForXcode.xcarchive" | |
| EXPORT_PATH="build/Export" | |
| mkdir -p "$EXPORT_PATH" | |
| # Copy app from archive | |
| cp -R "$ARCHIVE_PATH/Products/Applications/SwiftFormat for Xcode.app" "$EXPORT_PATH/" | |
| APP_PATH="$EXPORT_PATH/SwiftFormat for Xcode.app" | |
| echo "app-path=$APP_PATH" >> $GITHUB_OUTPUT | |
| echo "Copied app to: $APP_PATH" | |
| # Use app from archive as-is without additional signing | |
| echo "Using app from archive without modification" | |
| # Check and fix XcodeKit.framework structure | |
| XCODE_KIT_PATH="$APP_PATH/Contents/PlugIns/SwiftFormat.appex/Contents/Frameworks/XcodeKit.framework" | |
| echo "Checking XcodeKit.framework structure..." | |
| ls -la "$XCODE_KIT_PATH/Versions/" | |
| file "$XCODE_KIT_PATH/Versions/Current" | |
| # Validate the signature from archive | |
| echo "Validating archive signature..." | |
| if ! codesign --verify --deep --strict "$APP_PATH"; then | |
| echo "ERROR: Archive produced invalid signature!" | |
| exit 1 | |
| fi | |
| echo "Archive signature is valid" | |
| # Check Gatekeeper assessment | |
| echo "Checking Gatekeeper assessment..." | |
| if ! spctl -a -v "$APP_PATH"; then | |
| echo "ERROR: Gatekeeper would reject this app!" | |
| exit 1 | |
| fi | |
| echo "Gatekeeper assessment passed" | |
| - name: Notarize App | |
| uses: lando/notarize-action@v2 | |
| with: | |
| product-path: ${{ steps.export-app.outputs.app-path }} | |
| appstore-connect-username: ${{ secrets.NOTARIZATION_USERNAME }} | |
| appstore-connect-password: ${{ secrets.NOTARIZATION_PASSWORD }} | |
| appstore-connect-team-id: 8VQKF583ED | |
| primary-bundle-id: com.nicklockwood.SwiftFormat-for-Xcode | |
| verbose: true | |
| - name: Staple Notarization | |
| id: staple | |
| run: | | |
| xcrun stapler staple "${{ steps.export-app.outputs.app-path }}" | |
| - name: Get Notarization Logs | |
| if: failure() | |
| run: | | |
| echo "Getting notarization history and logs..." | |
| # Show recent history | |
| echo "=== Recent Notarization History ===" | |
| xcrun notarytool history --team-id 8VQKF583ED --apple-id "${{ secrets.NOTARIZATION_USERNAME }}" --password "${{ secrets.NOTARIZATION_PASSWORD }}" | |
| # Get the most recent request UUID and its detailed logs | |
| echo "" | |
| echo "=== Getting detailed logs for most recent submission ===" | |
| REQUEST_UUID=$(xcrun notarytool history --team-id 8VQKF583ED --apple-id "${{ secrets.NOTARIZATION_USERNAME }}" --password "${{ secrets.NOTARIZATION_PASSWORD }}" | grep -E "id:\s*[a-f0-9-]{36}" | head -1 | sed 's/.*id: //') | |
| if [ -n "$REQUEST_UUID" ]; then | |
| echo "Getting logs for request: $REQUEST_UUID" | |
| xcrun notarytool log "$REQUEST_UUID" --team-id 8VQKF583ED --apple-id "${{ secrets.NOTARIZATION_USERNAME }}" --password "${{ secrets.NOTARIZATION_PASSWORD }}" | |
| else | |
| echo "Could not extract request UUID from history" | |
| fi | |
| - name: Zip App | |
| run: | | |
| cd "$(dirname "${{ steps.export-app.outputs.app-path }}")" | |
| zip -r --symlinks SwiftFormat.for.Xcode.app.zip "SwiftFormat for Xcode.app" | |
| echo "ZIP_DIR=$(pwd)" >> $GITHUB_ENV | |
| - name: Upload App Artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: SwiftFormat-for-Xcode | |
| path: ${{ env.ZIP_DIR }}/SwiftFormat.for.Xcode.app.zip | |
| upload: | |
| name: Upload release artifacts | |
| runs-on: ubuntu-latest | |
| needs: [build-and-notarize] | |
| steps: | |
| - name: Checkout the repository | |
| uses: actions/checkout@v5 | |
| with: | |
| ref: ${{ github.event.release.tag_name || inputs.tag }} | |
| - uses: actions/download-artifact@v5 | |
| with: | |
| path: downloaded_artifacts | |
| pattern: SwiftFormat-for-Xcode | |
| - name: Display structure of downloaded files | |
| run: ls -R downloaded_artifacts | |
| - name: Get Release ID | |
| id: get-release-id | |
| run: | | |
| if [ -n "${{ github.event.release.id }}" ]; then | |
| echo "release-id=${{ github.event.release.id }}" >> $GITHUB_OUTPUT | |
| else | |
| TAG="${{ inputs.tag }}" | |
| RELEASE_ID=$(gh api repos/${{ github.repository }}/releases/tags/$TAG --jq '.id') | |
| echo "release-id=$RELEASE_ID" >> $GITHUB_OUTPUT | |
| fi | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Upload release assets | |
| uses: skx/github-action-publish-binaries@master | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| with: | |
| releaseId: ${{ steps.get-release-id.outputs.release-id }} | |
| args: 'downloaded_artifacts/SwiftFormat.for.Xcode.app.zip' |