Skip to content

Deploy iOS to TestFlight #8

Deploy iOS to TestFlight

Deploy iOS to TestFlight #8

name: Deploy iOS to TestFlight
on:
workflow_dispatch:
inputs:
version:
description: "Version number (e.g. 1.7.0)"
required: true
type: string
jobs:
build-and-deploy:
name: Build & Upload to TestFlight
runs-on: macos-latest
timeout-minutes: 45
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
- name: Set up Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable
- name: Create .env file
env:
GOOGLE_IOS_CLIENT_ID: ${{ secrets.GOOGLE_IOS_CLIENT_ID }}
DEEPGRAM_API_KEY: ${{ secrets.DEEPGRAM_API_KEY }}
run: |
echo "GOOGLE_IOS_CLIENT_ID=$GOOGLE_IOS_CLIENT_ID" > .env
echo "DEEPGRAM_API_KEY=$DEEPGRAM_API_KEY" >> .env
- name: Install Flutter dependencies
run: flutter pub get
- name: Install CocoaPods dependencies
working-directory: ios
run: pod install
- name: Import signing certificate
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
# Decode certificate
CERTIFICATE_PATH=$RUNNER_TEMP/certificate.p12
echo -n "$MACOS_CERTIFICATE" | base64 --decode -o "$CERTIFICATE_PATH"
# Create temporary keychain
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
# Import certificate into keychain
security import "$CERTIFICATE_PATH" \
-P "$MACOS_CERTIFICATE_PWD" \
-A \
-t cert \
-f pkcs12 \
-k "$KEYCHAIN_PATH"
# Allow codesign to access the keychain
security set-key-partition-list -S apple-tool:,apple:,codesign: \
-s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
# Add keychain to search list
security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | tr -d '"')
# Extract signing identity name from imported certificate
SIGNING_IDENTITY=$(security find-identity -v -p codesigning "$KEYCHAIN_PATH" | head -1 | sed 's/.*"\(.*\)"/\1/')
echo "::add-mask::$SIGNING_IDENTITY"
echo "SIGNING_IDENTITY=$SIGNING_IDENTITY" >> $GITHUB_ENV
- name: Install provisioning profile
env:
IOS_PROVISIONING_PROFILE_BASE64: ${{ secrets.IOS_PROVISIONING_PROFILE_BASE64 }}
run: |
# Decode provisioning profile
PROFILE_PATH=$RUNNER_TEMP/profile.mobileprovision
echo -n "$IOS_PROVISIONING_PROFILE_BASE64" | base64 --decode -o "$PROFILE_PATH"
# Extract UUID from provisioning profile
PROFILE_UUID=$(/usr/libexec/PlistBuddy -c "Print :UUID" /dev/stdin <<< \
$(security cms -D -i "$PROFILE_PATH"))
echo "PROFILE_UUID=$PROFILE_UUID" >> $GITHUB_ENV
# Install provisioning profile
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp "$PROFILE_PATH" ~/Library/MobileDevice/Provisioning\ Profiles/"$PROFILE_UUID".mobileprovision
- name: Set build number
run: |
BUILD_NUMBER=$(date +%Y%m%d%H%M)
echo "BUILD_NUMBER=$BUILD_NUMBER" >> $GITHUB_ENV
echo "Version: ${{ inputs.version }}+$BUILD_NUMBER"
- name: Build Flutter iOS (no codesign)
run: |
flutter build ios \
--release \
--no-codesign \
--build-name=${{ inputs.version }} \
--build-number=$BUILD_NUMBER
- name: Archive with Xcode
env:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
set -euo pipefail
xcodebuild archive \
-workspace ios/Runner.xcworkspace \
-scheme Runner \
-configuration Release \
-archivePath "$RUNNER_TEMP/Runner.xcarchive" \
-destination "generic/platform=iOS" \
CODE_SIGN_STYLE=Manual \
CODE_SIGN_IDENTITY="$SIGNING_IDENTITY" \
PROVISIONING_PROFILE_SPECIFIER="" \
PROVISIONING_PROFILE="$PROFILE_UUID" \
DEVELOPMENT_TEAM="$APPLE_TEAM_ID"
- name: Generate ExportOptions.plist
env:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
cat > $RUNNER_TEMP/ExportOptions.plist <<PLISTEOF
<?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</string>
<key>teamID</key>
<string>$APPLE_TEAM_ID</string>
<key>uploadSymbols</key>
<true/>
<key>provisioningProfiles</key>
<dict>
<key>com.kajabi.noa</key>
<string>$PROFILE_UUID</string>
</dict>
</dict>
</plist>
PLISTEOF
- name: Export IPA
run: |
xcodebuild -exportArchive \
-archivePath "$RUNNER_TEMP/Runner.xcarchive" \
-exportOptionsPlist "$RUNNER_TEMP/ExportOptions.plist" \
-exportPath "$RUNNER_TEMP/export"
- name: Upload to TestFlight
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
run: |
# Find the IPA
IPA_FILE=$(find $RUNNER_TEMP/export -name "*.ipa" -print -quit)
xcrun altool --upload-app \
--type ios \
--file "$IPA_FILE" \
-u "$APPLE_ID" \
-p "$APPLE_ID_PASSWORD"
- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: |
# Find the IPA file (name may vary)
IPA_FILE=$(find $RUNNER_TEMP/export -name "*.ipa" -print -quit)
gh release create "v${{ inputs.version }}" \
"$IPA_FILE#noa-${{ inputs.version }}.ipa" \
--title "v${{ inputs.version }}" \
--notes "Release v${{ inputs.version }} - deployed to TestFlight" \
--generate-notes
- name: Cleanup
if: always()
run: |
# Delete temporary keychain
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
if [ -f "$KEYCHAIN_PATH" ]; then
security delete-keychain "$KEYCHAIN_PATH" 2>/dev/null || true
fi
# Remove provisioning profile
if [ -n "$PROFILE_UUID" ]; then
rm -f ~/Library/MobileDevice/Provisioning\ Profiles/"$PROFILE_UUID".mobileprovision 2>/dev/null || true
fi
# Remove certificate
rm -f $RUNNER_TEMP/certificate.p12 2>/dev/null || true
# Remove .env
rm -f .env 2>/dev/null || true