Native macOS TestFlight #14
Workflow file for this run
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: Native macOS TestFlight | |
| 'on': | |
| push: | |
| tags: | |
| - 'macos-v*' | |
| workflow_dispatch: | |
| inputs: | |
| git_ref: | |
| description: "Git ref to build (branch, tag, or SHA)" | |
| required: false | |
| default: "" | |
| upload_to_testflight: | |
| description: "Upload exported package to TestFlight" | |
| required: true | |
| type: boolean | |
| default: true | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: native-macos-testflight-${{ github.ref }} | |
| cancel-in-progress: false | |
| jobs: | |
| build-and-upload: | |
| name: Build signed macOS package | |
| runs-on: macos-15 | |
| env: | |
| APPLE_TEAM_ID: MM5YXC7T6E | |
| BUNDLE_ID: com.shinycomputers.everycodecompanion | |
| SCHEME: CodeNativeMac | |
| XCODE_PROJECT: native/CodeNativeMac/CodeNativeMac.xcodeproj | |
| ARCHIVE_PATH: /tmp/ecc-macos/EveryCodeCompanion.xcarchive | |
| EXPORT_PATH: /tmp/ecc-macos/export | |
| EXPORT_OPTIONS_PATH: /tmp/ecc-macos/ExportOptions.plist | |
| PROFILE_PATH: /tmp/ecc-macos/every-code-companion-macos.provisionprofile | |
| steps: | |
| - name: Resolve checkout ref | |
| id: resolve_ref | |
| run: | | |
| set -euo pipefail | |
| ref="$GITHUB_REF" | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ] \ | |
| && [ -n "${{ github.event.inputs.git_ref }}" ]; then | |
| ref="${{ github.event.inputs.git_ref }}" | |
| fi | |
| echo "value=$ref" >> "$GITHUB_OUTPUT" | |
| - name: Prepare build directories | |
| run: | | |
| set -euo pipefail | |
| mkdir -p "$(dirname "$ARCHIVE_PATH")" | |
| mkdir -p "$EXPORT_PATH" | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ steps.resolve_ref.outputs.value }} | |
| - name: Select Xcode | |
| uses: maxim-lobanov/setup-xcode@v1 | |
| with: | |
| xcode-version: latest-stable | |
| - name: Validate required secrets | |
| env: | |
| IOS_DIST_CERT_P12_BASE64: ${{ secrets.IOS_DIST_CERT_P12_BASE64 }} | |
| IOS_DIST_CERT_PASSWORD: ${{ secrets.IOS_DIST_CERT_PASSWORD }} | |
| MACOS_INSTALLER_CERT_P12_BASE64: | |
| ${{ secrets.MACOS_INSTALLER_CERT_P12_BASE64 }} | |
| MACOS_INSTALLER_CERT_PASSWORD: | |
| ${{ secrets.MACOS_INSTALLER_CERT_PASSWORD }} | |
| MACOS_APPSTORE_PROFILE_BASE64: | |
| ${{ secrets.MACOS_APPSTORE_PROFILE_BASE64 }} | |
| MACOS_APPSTORE_PROFILE_NAME: | |
| ${{ secrets.MACOS_APPSTORE_PROFILE_NAME }} | |
| APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }} | |
| APP_STORE_CONNECT_ISSUER_ID: | |
| ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }} | |
| APP_STORE_CONNECT_PRIVATE_KEY: | |
| ${{ secrets.APP_STORE_CONNECT_PRIVATE_KEY }} | |
| run: | | |
| set -euo pipefail | |
| missing=() | |
| for name in \ | |
| IOS_DIST_CERT_P12_BASE64 \ | |
| IOS_DIST_CERT_PASSWORD \ | |
| MACOS_INSTALLER_CERT_P12_BASE64 \ | |
| MACOS_INSTALLER_CERT_PASSWORD \ | |
| MACOS_APPSTORE_PROFILE_BASE64 \ | |
| MACOS_APPSTORE_PROFILE_NAME \ | |
| APP_STORE_CONNECT_KEY_ID \ | |
| APP_STORE_CONNECT_ISSUER_ID \ | |
| APP_STORE_CONNECT_PRIVATE_KEY; do | |
| if [ -z "${!name:-}" ]; then | |
| missing+=("$name") | |
| fi | |
| done | |
| if [ "${#missing[@]}" -gt 0 ]; then | |
| printf 'Missing required secrets:\n' >&2 | |
| printf ' - %s\n' "${missing[@]}" >&2 | |
| exit 1 | |
| fi | |
| - name: Import Apple Distribution certificate | |
| uses: apple-actions/import-codesign-certs@v3 | |
| with: | |
| p12-file-base64: ${{ secrets.IOS_DIST_CERT_P12_BASE64 }} | |
| p12-password: ${{ secrets.IOS_DIST_CERT_PASSWORD }} | |
| keychain: signing_temp | |
| keychain-password: ${{ github.run_id }} | |
| - name: Import Mac Installer certificate | |
| uses: apple-actions/import-codesign-certs@v3 | |
| with: | |
| p12-file-base64: ${{ secrets.MACOS_INSTALLER_CERT_P12_BASE64 }} | |
| p12-password: ${{ secrets.MACOS_INSTALLER_CERT_PASSWORD }} | |
| keychain: signing_temp | |
| keychain-password: ${{ github.run_id }} | |
| create-keychain: false | |
| - name: Verify installer certificate availability | |
| run: | | |
| set -euo pipefail | |
| has_modern=0 | |
| has_legacy=0 | |
| if security find-certificate \ | |
| -a \ | |
| -c "Mac Installer Distribution" \ | |
| >/dev/null 2>&1; then | |
| has_modern=1 | |
| fi | |
| if security find-certificate \ | |
| -a \ | |
| -c "3rd Party Mac Developer Installer" \ | |
| >/dev/null 2>&1; then | |
| has_legacy=1 | |
| fi | |
| if [ "$has_modern" -eq 0 ] && [ "$has_legacy" -eq 0 ]; then | |
| echo "Missing Mac Installer Distribution certificate" \ | |
| "in keychain." >&2 | |
| echo "Provide MACOS_INSTALLER_CERT_P12_BASE64/" >&2 | |
| echo "MACOS_INSTALLER_CERT_PASSWORD, or include installer cert" >&2 | |
| echo "in IOS_DIST_CERT_P12_BASE64 before rerunning." >&2 | |
| exit 1 | |
| fi | |
| - name: Install Mac App Store provisioning profile | |
| env: | |
| MACOS_APPSTORE_PROFILE_BASE64: | |
| ${{ secrets.MACOS_APPSTORE_PROFILE_BASE64 }} | |
| run: | | |
| set -euo pipefail | |
| mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles" | |
| echo "$MACOS_APPSTORE_PROFILE_BASE64" | base64 --decode \ | |
| > "$PROFILE_PATH" | |
| profile_plist=$(security cms -D -i "$PROFILE_PATH") | |
| profile_uuid=$( | |
| /usr/libexec/PlistBuddy -c 'Print UUID' /dev/stdin \ | |
| <<<"$profile_plist" | |
| ) | |
| profiles_dir="$HOME/Library/MobileDevice/Provisioning Profiles" | |
| cp "$PROFILE_PATH" "$profiles_dir/$profile_uuid.provisionprofile" | |
| - name: Write export options | |
| env: | |
| MACOS_APPSTORE_PROFILE_NAME: | |
| ${{ secrets.MACOS_APPSTORE_PROFILE_NAME }} | |
| run: | | |
| set -euo pipefail | |
| cat > "$EXPORT_OPTIONS_PATH" <<EOF | |
| <?xml version="1.0" encoding="UTF-8"?> | |
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" | |
| "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
| <plist version="1.0"> | |
| <dict> | |
| <key>destination</key> | |
| <string>export</string> | |
| <key>method</key> | |
| <string>app-store-connect</string> | |
| <key>signingCertificate</key> | |
| <string>Apple Distribution</string> | |
| <key>installerSigningCertificate</key> | |
| <string>3rd Party Mac Developer Installer</string> | |
| <key>signingStyle</key> | |
| <string>manual</string> | |
| <key>stripSwiftSymbols</key> | |
| <true/> | |
| <key>teamID</key> | |
| <string>${APPLE_TEAM_ID}</string> | |
| <key>provisioningProfiles</key> | |
| <dict> | |
| <key>${BUNDLE_ID}</key> | |
| <string>${MACOS_APPSTORE_PROFILE_NAME}</string> | |
| </dict> | |
| </dict> | |
| </plist> | |
| EOF | |
| - name: Archive macOS app | |
| env: | |
| MACOS_APPSTORE_PROFILE_NAME: | |
| ${{ secrets.MACOS_APPSTORE_PROFILE_NAME }} | |
| run: | | |
| set -euo pipefail | |
| xcodebuild \ | |
| -project "$XCODE_PROJECT" \ | |
| -scheme "$SCHEME" \ | |
| -configuration Release \ | |
| -destination "generic/platform=macOS" \ | |
| -archivePath "$ARCHIVE_PATH" \ | |
| DEVELOPMENT_TEAM="$APPLE_TEAM_ID" \ | |
| PRODUCT_BUNDLE_IDENTIFIER="$BUNDLE_ID" \ | |
| CODE_SIGN_STYLE=Manual \ | |
| CODE_SIGN_IDENTITY="Apple Distribution" \ | |
| PROVISIONING_PROFILE_SPECIFIER="$MACOS_APPSTORE_PROFILE_NAME" \ | |
| clean archive | |
| - name: Export package | |
| run: | | |
| set -euo pipefail | |
| xcodebuild \ | |
| -exportArchive \ | |
| -archivePath "$ARCHIVE_PATH" \ | |
| -exportPath "$EXPORT_PATH" \ | |
| -exportOptionsPlist "$EXPORT_OPTIONS_PATH" | |
| pkg_path=$(find "$EXPORT_PATH" -maxdepth 1 -name '*.pkg' -print -quit) | |
| if [ -z "$pkg_path" ]; then | |
| echo "No PKG produced by export" >&2 | |
| exit 1 | |
| fi | |
| mv "$pkg_path" "$EXPORT_PATH/EveryCodeCompanion.pkg" | |
| - name: Install App Store Connect API key | |
| env: | |
| APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }} | |
| APP_STORE_CONNECT_PRIVATE_KEY: | |
| ${{ secrets.APP_STORE_CONNECT_PRIVATE_KEY }} | |
| run: | | |
| set -euo pipefail | |
| key_dir="$HOME/.appstoreconnect/private_keys" | |
| key_path="$key_dir/AuthKey_${APP_STORE_CONNECT_KEY_ID}.p8" | |
| mkdir -p "$key_dir" | |
| printf '%s' "$APP_STORE_CONNECT_PRIVATE_KEY" > "$key_path" | |
| chmod 600 "$key_path" | |
| - name: Upload package artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: EveryCodeCompanion-macos-pkg | |
| path: | | |
| ${{ env.EXPORT_PATH }}/EveryCodeCompanion.pkg | |
| ${{ env.ARCHIVE_PATH }}/dSYMs | |
| if-no-files-found: error | |
| - name: Upload to TestFlight | |
| if: >- | |
| ${{ github.event_name == 'push' | |
| || github.event.inputs.upload_to_testflight == 'true' }} | |
| env: | |
| APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }} | |
| APP_STORE_CONNECT_ISSUER_ID: | |
| ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }} | |
| run: | | |
| set -euo pipefail | |
| log_path=/tmp/ecc-macos-upload.log | |
| xcrun altool --output-format xml \ | |
| --upload-app \ | |
| --file "$EXPORT_PATH/EveryCodeCompanion.pkg" \ | |
| --type macos \ | |
| --apiKey "$APP_STORE_CONNECT_KEY_ID" \ | |
| --apiIssuer "$APP_STORE_CONNECT_ISSUER_ID" \ | |
| 2>&1 | tee "$log_path" | |
| if grep -Eq \ | |
| "UPLOAD FAILED|Validation failed| ERROR:" \ | |
| "$log_path"; then | |
| echo "Detected upload failure markers in altool output" >&2 | |
| exit 1 | |
| fi | |
| if ! grep -Eq "UPLOAD SUCCEEDED with no errors" \ | |
| "$log_path"; then | |
| echo "Upload result did not include success marker" >&2 | |
| exit 1 | |
| fi |