Deploy iOS (TestFlight) #4
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
| # Deploy iOS (TestFlight) | |
| # | |
| # MARKETING_VERSION and CURRENT_PROJECT_VERSION for archives come from root CMakeLists.txt (extract-version), | |
| # not from the 0.0.0 / 0 placeholders in ios/Pylux.xcodeproj — see "Set iOS marketing + build numbers" + xcodebuild below. | |
| # | |
| # Architecture: arm64 (device only, no simulator) | |
| # Output: Signed IPA uploaded to TestFlight via Fastlane | |
| # release/beta → upload only (manual submit) | |
| # master → upload and auto-submit for review | |
| # | |
| # Required secrets: | |
| # IOS_CERTIFICATE_P12_BASE64 - base64 of distribution .p12 | |
| # IOS_CERTIFICATE_PASSWORD - password for .p12 | |
| # IOS_PROVISIONING_PROFILE_BASE64 - base64 of App Store provisioning profile | |
| # APP_STORE_CONNECT_API_KEY_KEY_ID - App Store Connect API key ID | |
| # APP_STORE_CONNECT_API_KEY_ISSUER_ID - App Store Connect API issuer ID | |
| # APP_STORE_CONNECT_API_KEY_KEY_BASE64 - base64 of .p8 key file | |
| name: Deploy iOS (TestFlight) | |
| on: | |
| workflow_dispatch: | |
| workflow_call: | |
| outputs: | |
| chiaki_version: | |
| description: Pylux version from CMakeLists.txt | |
| value: ${{ jobs.build-ios.outputs.chiaki_version }} | |
| testflight_join_url: | |
| description: Public TestFlight join link for external testers | |
| value: ${{ jobs.build-ios.outputs.testflight_join_url }} | |
| ios_upload_succeeded: | |
| description: Whether the TestFlight upload step succeeded | |
| value: ${{ jobs.build-ios.outputs.ios_upload_succeeded }} | |
| concurrency: | |
| group: build-ios-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| build-ios: | |
| name: Build and deploy to TestFlight | |
| runs-on: macos-26 | |
| timeout-minutes: 120 | |
| outputs: | |
| chiaki_version: ${{ steps.extract_version.outputs.version }} | |
| testflight_join_url: ${{ steps.release_metadata.outputs.testflight_join_url }} | |
| ios_upload_succeeded: ${{ steps.upload_testflight.outcome == 'success' }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| submodules: recursive | |
| - name: Extract version | |
| id: extract_version | |
| uses: ./.github/actions/extract-version | |
| - name: Release metadata outputs | |
| id: release_metadata | |
| if: always() | |
| run: | | |
| echo "testflight_join_url=https://testflight.apple.com/join/V8pv6cXK" >> "$GITHUB_OUTPUT" | |
| - name: Install build dependencies | |
| run: | | |
| brew install cmake ninja protobuf@29 python-setuptools | |
| pip3 install --user --break-system-packages 'protobuf>=5,<6' | |
| echo "$(brew --prefix)/opt/protobuf@29/bin" >> "$GITHUB_PATH" | |
| - name: Download iOS toolchain | |
| working-directory: ios | |
| run: | | |
| if [ ! -f ios.toolchain.cmake ]; then | |
| curl -sL -o ios.toolchain.cmake \ | |
| https://raw.githubusercontent.com/leetal/ios-cmake/master/ios.toolchain.cmake | |
| fi | |
| - name: Build chiaki-lib (device) | |
| working-directory: ios | |
| run: | | |
| cmake -S . -B build-iphoneos -G Ninja \ | |
| -DCMAKE_TOOLCHAIN_FILE=ios.toolchain.cmake \ | |
| -DPLATFORM=OS64 \ | |
| -DCMAKE_BUILD_TYPE=Release \ | |
| -DUSE_LIBIDN2=OFF \ | |
| -DCURL_USE_LIBPSL=OFF \ | |
| -DUSE_NGHTTP2=OFF \ | |
| -DCMAKE_POLICY_VERSION_MINIMUM=3.5 | |
| cmake --build build-iphoneos --config Release --target chiaki-lib | |
| - name: Create combined static library | |
| working-directory: ios | |
| run: | | |
| find build-iphoneos -name "*.a" -print0 | \ | |
| xargs -0 libtool -static -o build-iphoneos/libchiaki_complete.a | |
| - name: Import code signing certificate | |
| env: | |
| P12_BASE64: ${{ secrets.IOS_CERTIFICATE_P12_BASE64 }} | |
| P12_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }} | |
| run: | | |
| if [ -z "$P12_BASE64" ]; then | |
| echo "::error::IOS_CERTIFICATE_P12_BASE64 secret is not set" | |
| exit 1 | |
| fi | |
| CERT_PATH="$RUNNER_TEMP/cert.p12" | |
| KEYCHAIN_PATH="$RUNNER_TEMP/ios-signing.keychain-db" | |
| KEYCHAIN_PW="$(openssl rand -hex 16)" | |
| echo "$P12_BASE64" > "$RUNNER_TEMP/cert.b64" | |
| base64 -D -i "$RUNNER_TEMP/cert.b64" -o "$CERT_PATH" | |
| rm -f "$RUNNER_TEMP/cert.b64" | |
| security create-keychain -p "$KEYCHAIN_PW" "$KEYCHAIN_PATH" | |
| security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" | |
| security unlock-keychain -p "$KEYCHAIN_PW" "$KEYCHAIN_PATH" | |
| security import "$CERT_PATH" -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH" | |
| security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PW" "$KEYCHAIN_PATH" | |
| security list-keychain -d user -s "$KEYCHAIN_PATH" login.keychain-db | |
| - name: Install provisioning profile | |
| env: | |
| PROFILE_BASE64: ${{ secrets.IOS_PROVISIONING_PROFILE_BASE64 }} | |
| run: | | |
| if [ -z "$PROFILE_BASE64" ]; then | |
| echo "::error::IOS_PROVISIONING_PROFILE_BASE64 secret is not set" | |
| exit 1 | |
| fi | |
| PROFILE_PATH="$RUNNER_TEMP/profile.mobileprovision" | |
| echo "$PROFILE_BASE64" > "$RUNNER_TEMP/profile.b64" | |
| base64 -D -i "$RUNNER_TEMP/profile.b64" -o "$PROFILE_PATH" | |
| rm -f "$RUNNER_TEMP/profile.b64" | |
| mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles" | |
| UUID=$(/usr/libexec/PlistBuddy -c "Print UUID" /dev/stdin <<< \ | |
| "$(security cms -D -i "$PROFILE_PATH" 2>/dev/null)") | |
| cp "$PROFILE_PATH" "$HOME/Library/MobileDevice/Provisioning Profiles/${UUID}.mobileprovision" | |
| echo "PROVISIONING_PROFILE_UUID=$UUID" >> "$GITHUB_ENV" | |
| echo "Installed provisioning profile: $UUID" | |
| - name: Write App Store Connect API key | |
| env: | |
| ASC_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_BASE64 }} | |
| ASC_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_ID }} | |
| run: | | |
| if [ -z "$ASC_KEY_BASE64" ] || [ -z "$ASC_KEY_ID" ]; then | |
| echo "::error::App Store Connect API key secrets are not set" | |
| exit 1 | |
| fi | |
| KEY_DIR="$RUNNER_TEMP/asc-keys" | |
| mkdir -p "$KEY_DIR" | |
| echo "$ASC_KEY_BASE64" > "$RUNNER_TEMP/key.b64" | |
| base64 -D -i "$RUNNER_TEMP/key.b64" -o "$KEY_DIR/AuthKey_${ASC_KEY_ID}.p8" | |
| rm -f "$RUNNER_TEMP/key.b64" | |
| echo "APP_STORE_CONNECT_API_KEY_KEY_FILEPATH=$KEY_DIR/AuthKey_${ASC_KEY_ID}.p8" >> "$GITHUB_ENV" | |
| # pbxproj keeps 0.0.0 / 0 as non-authoritative placeholders; these env vars override at xcodebuild archive time. | |
| - name: Set iOS marketing + build numbers from CMake (root CMakeLists.txt) | |
| run: | | |
| echo "IOS_MARKETING_VERSION=${{ steps.extract_version.outputs.version }}" >> "$GITHUB_ENV" | |
| echo "IOS_CURRENT_PROJECT_VERSION=${{ steps.extract_version.outputs.semver_build_id }}" >> "$GITHUB_ENV" | |
| echo "iOS MARKETING_VERSION=${{ steps.extract_version.outputs.version }} (CFBundleShortVersionString)" | |
| echo "iOS CURRENT_PROJECT_VERSION=${{ steps.extract_version.outputs.semver_build_id }} (CFBundleVersion / same encoding as Android versionCode)" | |
| - name: Archive | |
| working-directory: ios | |
| run: | | |
| xcodebuild -project Pylux.xcodeproj \ | |
| -scheme Pylux \ | |
| -sdk iphoneos \ | |
| -configuration Release \ | |
| archive \ | |
| -destination 'generic/platform=iOS' \ | |
| -archivePath build-derived/Pylux.xcarchive \ | |
| -derivedDataPath build-derived \ | |
| CODE_SIGN_STYLE=Manual \ | |
| CODE_SIGN_IDENTITY="Apple Distribution" \ | |
| DEVELOPMENT_TEAM=KG7LUU8FX7 \ | |
| PROVISIONING_PROFILE=${{ env.PROVISIONING_PROFILE_UUID }} \ | |
| MARKETING_VERSION="${IOS_MARKETING_VERSION}" \ | |
| CURRENT_PROJECT_VERSION="${IOS_CURRENT_PROJECT_VERSION}" | |
| - name: Export IPA | |
| working-directory: ios | |
| run: | | |
| cat > build-derived/CIExportOptions.plist <<PLIST | |
| <?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>method</key> | |
| <string>app-store-connect</string> | |
| <key>teamID</key> | |
| <string>KG7LUU8FX7</string> | |
| <key>signingStyle</key> | |
| <string>manual</string> | |
| <key>provisioningProfiles</key> | |
| <dict> | |
| <key>com.pylux.stream</key> | |
| <string>${{ env.PROVISIONING_PROFILE_UUID }}</string> | |
| </dict> | |
| <key>uploadSymbols</key> | |
| <true/> | |
| <key>stripSwiftSymbols</key> | |
| <true/> | |
| </dict> | |
| </plist> | |
| PLIST | |
| xcodebuild -exportArchive \ | |
| -archivePath build-derived/Pylux.xcarchive \ | |
| -exportPath build-derived/export \ | |
| -exportOptionsPlist build-derived/CIExportOptions.plist | |
| IPA=$(find "$PWD/build-derived/export" -maxdepth 1 -name "*.ipa" | head -1) | |
| if [ -z "$IPA" ] || [ ! -f "$IPA" ]; then | |
| echo "::error::Export failed — no .ipa found" | |
| exit 1 | |
| fi | |
| echo "IPA_PATH=$IPA" >> "$GITHUB_ENV" | |
| echo "Exported IPA: $IPA" | |
| - name: Upload to TestFlight | |
| id: upload_testflight | |
| working-directory: ios | |
| env: | |
| APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_ID }} | |
| APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }} | |
| PYLUX_SUBMIT_FOR_REVIEW: ${{ github.ref_name == 'master' && 'true' || 'false' }} | |
| PYLUX_TESTFLIGHT_EXTERNAL_BETA: ${{ github.ref_name == 'release/beta' && 'true' || 'false' }} | |
| run: | | |
| brew install fastlane | |
| export PYLUX_IPA_PATH="${{ env.IPA_PATH }}" | |
| fastlane upload_pylux_ipa | |
| - name: Deployment summary | |
| if: always() | |
| run: | | |
| { | |
| echo "## Pylux iOS — v${CHIAKI_VERSION:-?}" | |
| echo "" | |
| if [ "${{ steps.upload_testflight.outcome }}" = "success" ]; then | |
| if [ "${{ github.ref_name }}" = "master" ]; then | |
| echo "Submitted for App Store review. Once approved, download here:" | |
| echo "" | |
| echo "**[Download on the App Store](https://apps.apple.com/us/app/pylux-remote-play/id6761292658)**" | |
| echo "" | |
| echo "> If recently submitted, allow a few days for Apple review before it appears." | |
| else | |
| echo "Beta build uploaded to TestFlight." | |
| echo "" | |
| echo "**[Join TestFlight beta](https://testflight.apple.com/join/V8pv6cXK)**" | |
| echo "" | |
| echo "1. Install [TestFlight](https://apps.apple.com/app/testflight/id899247664) on your iPhone if you haven't already." | |
| echo "2. Open the join link above and accept the invite." | |
| echo "3. Install Pylux from TestFlight. New builds may take a short time to pass Beta App Review before appearing." | |
| fi | |
| else | |
| echo "Upload did not complete (outcome: **${{ steps.upload_testflight.outcome }}**). Check the logs above for details." | |
| fi | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Cleanup keychain | |
| if: always() | |
| run: | | |
| security delete-keychain "$RUNNER_TEMP/ios-signing.keychain-db" 2>/dev/null || true |