Skip to content

Deploy iOS (TestFlight) #4

Deploy iOS (TestFlight)

Deploy iOS (TestFlight) #4

Workflow file for this run

# 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