Skip to content

Instantly share code, notes, and snippets.

@husnaintahir
Created October 15, 2025 13:43
Show Gist options
  • Select an option

  • Save husnaintahir/2b9af02e2a20cf5e8afa0093e4f07226 to your computer and use it in GitHub Desktop.

Select an option

Save husnaintahir/2b9af02e2a20cf5e8afa0093e4f07226 to your computer and use it in GitHub Desktop.
name: iOS TestFlight Deployment
on:
workflow_dispatch:
inputs:
release:
description: "Trigger release to TestFlight"
required: true
default: "true"
type: choice
options: [ "true", "false" ]
version:
description: "App version (e.g. 1.0.0)"
default: "0.1.4"
required: true
buildNumber:
description: "Build number (e.g. 1)"
default: "3"
required: true
jobs:
build-ios:
runs-on: macos-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Install dependencies
run: npm install
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '16.0'
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'
- name: Install CocoaPods
run: gem install cocoapods
- name: Patch xcodeproj for Xcode 16 compatibility
run: |
XCODEPROJ_PATH=$(gem which xcodeproj | sed 's/\.rb$//')
CONSTANTS_FILE="$XCODEPROJ_PATH/project/object/native_target.rb"
# Add support for object version 70
if [ -f "$CONSTANTS_FILE" ]; then
sed -i '' "s/LAST_KNOWN_OBJECT_VERSION = '[0-9]*'/LAST_KNOWN_OBJECT_VERSION = '70'/" "$CONSTANTS_FILE" 2>/dev/null || true
fi
# Alternative: directly patch the constants
ruby -i -pe "gsub(/LAST_KNOWN_OBJECT_VERSION = '[0-9]*'/, \"LAST_KNOWN_OBJECT_VERSION = '70'\")" $(gem which xcodeproj/project/object/native_target.rb) 2>/dev/null || true
- name: Install CocoaPods dependencies
run: |
cd ios
pod install --repo-update
cd ..
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: 'latest-stable'
- name: Create temporary keychain
run: |
security create-keychain -p "${{ secrets.APPLE_CERT_PASSWORD }}" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "${{ secrets.APPLE_CERT_PASSWORD }}" build.keychain
security set-keychain-settings -t 3600 -u build.keychain
- name: Import certificate
run: |
echo "${{ secrets.APPLE_CERT_P12 }}" | base64 --decode > certificate.p12
security import certificate.p12 -k build.keychain -P "${{ secrets.APPLE_CERT_PASSWORD }}" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "${{ secrets.APPLE_CERT_PASSWORD }}" build.keychain
rm certificate.p12
- name: Install provisioning profiles
run: |
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
# Decode and save provisioning profiles
echo "${{ secrets.APPLE_MATCH_PROVISIONING_PROFILE }}" | base64 --decode > ~/Library/MobileDevice/Provisioning\ Profiles/ios_distribution_provision.mobileprovision
echo "${{ secrets.APPLE_EXTENSION_PROVISION_PROFILE }}" | base64 --decode > ~/Library/MobileDevice/Provisioning\ Profiles/ios_extension_provision.mobileprovision
echo "βœ… Installed provisioning profiles:"
ls -l ~/Library/MobileDevice/Provisioning\ Profiles
echo "πŸ“„ Profile names detected:"
/usr/libexec/PlistBuddy -c "Print :Name" ~/Library/MobileDevice/Provisioning\ Profiles/ios_distribution_provision.mobileprovision || true
/usr/libexec/PlistBuddy -c "Print :Name" ~/Library/MobileDevice/Provisioning\ Profiles/ios_extension_provision.mobileprovision || true
- name: Update version and build number
run: |
cd ios
agvtool new-marketing-version ${{ github.event.inputs.version }}
agvtool new-version -all ${{ github.event.inputs.buildNumber }}
cd ..
- name: Patch provisioning profile in Xcode project
run: |
cd ios
echo "πŸ“„ Injecting provisioning profile into project settings..."
/usr/libexec/PlistBuddy -c "Set :objects:$(/usr/libexec/PlistBuddy -c 'Print :rootObject' MyApp.xcodeproj/project.pbxproj):attributes:TargetAttributes:$(grep -A1 'MyApp' MyApp.xcodeproj/project.pbxproj | grep -Eo '[A-F0-9]{24}' | head -1):ProvisioningStyle Manual" MyApp.xcodeproj/project.pbxproj || true
- name: Build iOS app
run: |
cd ios
xcodebuild -workspace MyApp.xcworkspace \
-scheme MyApp \
-configuration Release \
-destination generic/platform=iOS \
-archivePath $PWD/build/App.xcarchive \
clean archive \
CODE_SIGN_STYLE=Manual \
DEVELOPMENT_TEAM=[MY_TEAM_ID] \
CODE_SIGN_IDENTITY="Apple Distribution" \
OTHER_CODE_SIGN_FLAGS="--verbose" \
ONLY_ACTIVE_ARCH=NO
- name: Export IPA
run: |
cd ios
echo "πŸ“¦ Exporting IPA..."
xcodebuild -exportArchive \
-archivePath $PWD/build/App.xcarchive \
-exportOptionsPlist exportOptions.plist \
-exportPath $PWD/build \
-allowProvisioningUpdates \
-verbose
- name: Create App Store Connect API Key
run: |
mkdir -p ~/.appstoreconnect/private_keys
echo "${{ secrets.APPLE_APP_STORE_CONNECT_API_KEY }}" | base64 --decode > ~/.appstoreconnect/private_keys/AuthKey_${{ secrets.APPLE_APP_STORE_CONNECT_API_KEY_ID }}.p8
- name: Upload to TestFlight
if: github.event.inputs.release == 'true'
run: |
echo "πŸš€ Uploading to TestFlight..."
xcrun altool --upload-app \
-f ios/build/*.ipa \
-t ios \
--apiKey ${{ secrets.APPLE_APP_STORE_CONNECT_API_KEY_ID }} \
--apiIssuer ${{ secrets.APPLE_APP_STORE_CONNECT_API_ISSUER_ID }}
- name: Upload IPA as artifact
uses: actions/upload-artifact@v4
with:
name: ios-app-${{ github.event.inputs.version }}-${{ github.event.inputs.buildNumber }}
path: ios/build/*.ipa
retention-days: 30
- name: Cleanup
if: always()
run: |
security delete-keychain build.keychain
rm -rf ~/Library/MobileDevice/Provisioning\ Profiles
rm -rf ~/.appstoreconnect
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment