feat(release): add macOS TestFlight pipeline and target #1
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.macos | |
| 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: Generate macOS project | |
| run: | | |
| set -euo pipefail | |
| cd native/CodeNativeMac | |
| xcodegen generate | |
| - 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_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_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 }} | |
| - 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>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" \ | |
| | tee "$log_path" | |
| if rg -q "UPLOAD FAILED|Validation failed| ERROR:" "$log_path"; then | |
| echo "Detected upload failure markers in altool output" >&2 | |
| exit 1 | |
| fi | |
| if ! rg -q "UPLOAD SUCCEEDED with no errors" "$log_path"; then | |
| echo "Upload result did not include success marker" >&2 | |
| exit 1 | |
| fi |