DEV Community

Aleksandr Ilinskiy
Aleksandr Ilinskiy

Posted on

How to Deploy Your iOS App to TestFlight with GitHub Actions (Step-by-Step)

Setting up CI/CD for iOS is significantly harder than for web apps. You need macOS runners, the right Xcode version, code signing certificates, provisioning profiles, and App Store Connect API keys — all configured correctly in a YAML file.

This guide walks you through the complete setup, from zero to a working GitHub Actions workflow that builds your app and uploads it to TestFlight on every push to main.

Prerequisites

Before you start, make sure you have:

  • An Apple Developer account ($99/year)
  • Your app set up in App Store Connect
  • An Xcode project that builds locally
  • A GitHub repository

Step 1: Create an App Store Connect API Key

Apple deprecated password-based authentication. You need an API key.

  1. Go to App Store Connect → Users and Access → Keys
  2. Click Generate API Key
  3. Name it "GitHub Actions CI" and give it Developer role
  4. Download the .p8 file (you can only download it once!)
  5. Note the Key ID and Issuer ID from the page

Step 2: Export Your Code Signing Certificate

You need your distribution certificate as a base64-encoded .p12 file.

  1. Open Keychain Access on your Mac
  2. Find your "Apple Distribution" certificate
  3. Right-click → Export → save as .p12 with a password
  4. Base64-encode it:
base64 -i Certificates.p12 | pbcopy
Enter fullscreen mode Exit fullscreen mode

This copies the encoded string to your clipboard.

Step 3: Add GitHub Secrets

Go to your repo → Settings → Secrets and variables → Actions and add these secrets:

Secret Name Value
CERTIFICATES_P12 Base64-encoded .p12 certificate
CERTIFICATES_P12_PASSWORD Password you set when exporting
APPSTORE_ISSUER_ID From App Store Connect API page
APPSTORE_KEY_ID From App Store Connect API page
APPSTORE_PRIVATE_KEY Full contents of the .p8 file
APPLE_TEAM_ID Your 10-character Team ID

You can find your Team ID at developer.apple.com/account → Membership.

Step 4: Create the Workflow File

Create .github/workflows/ios-testflight.yml in your repository:

name: iOS — Build & Deploy to TestFlight

on:
  push:
    branches: [main]

jobs:
  build:
    name: Build & Upload to TestFlight
    runs-on: macos-14

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Cache CocoaPods
        uses: actions/cache@v4
        with:
          path: Pods
          key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
          restore-keys: |
            ${{ runner.os }}-pods-

      - name: Install CocoaPods
        run: pod install --repo-update

      - name: Select Xcode 15.2
        uses: maxim-lobanov/setup-xcode@v1
        with:
          xcode-version: '15.2'

      - name: Import Code-Signing Certificate
        uses: apple-actions/import-codesign-certs@v3
        with:
          p12-file-base64: ${{ secrets.CERTIFICATES_P12 }}
          p12-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }}

      - name: Import Provisioning Profile
        uses: apple-actions/download-provisioning-profiles@v1
        with:
          bundle-id: 'com.yourcompany.yourapp'
          issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }}
          api-key-id: ${{ secrets.APPSTORE_KEY_ID }}
          api-private-key: ${{ secrets.APPSTORE_PRIVATE_KEY }}

      - name: Build & Archive
        run: |
          xcodebuild archive \
            -scheme YourApp \
            -configuration Release \
            -archivePath $RUNNER_TEMP/YourApp.xcarchive \
            -destination "generic/platform=iOS" \
            CODE_SIGN_STYLE=Manual \
            DEVELOPMENT_TEAM=${{ secrets.APPLE_TEAM_ID }}

      - name: Export IPA
        run: |
          xcodebuild -exportArchive \
            -archivePath $RUNNER_TEMP/YourApp.xcarchive \
            -exportPath $RUNNER_TEMP/export \
            -exportOptionsPlist ExportOptions.plist

      - name: Upload to TestFlight
        uses: apple-actions/upload-testflight-build@v1
        with:
          app-path: ${{ runner.temp }}/export/YourApp.ipa
          issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }}
          api-key-id: ${{ secrets.APPSTORE_KEY_ID }}
          api-private-key: ${{ secrets.APPSTORE_PRIVATE_KEY }}
Enter fullscreen mode Exit fullscreen mode

Important: Replace YourApp with your actual Xcode scheme name and com.yourcompany.yourapp with your Bundle ID.

Step 5: Create ExportOptions.plist

Create ExportOptions.plist in your project root:

<?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>destination</key>
    <string>export</string>
    <key>signingStyle</key>
    <string>manual</string>
    <key>teamID</key>
    <string>YOUR_TEAM_ID</string>
    <key>uploadSymbols</key>
    <true/>
</dict>
</plist>
Enter fullscreen mode Exit fullscreen mode

Step 6: Push and Watch

git add .
git commit -m "Add TestFlight CI/CD workflow"
git push origin main
Enter fullscreen mode Exit fullscreen mode

Go to your repo → Actions tab. You should see the workflow running. First build takes 10-15 minutes. If everything is configured correctly, your app will appear in TestFlight within 15-30 minutes after the build completes.

Common Issues

"No signing certificate found" — Your P12 wasn't imported correctly. Make sure you base64-encoded the entire file and the password matches.

"No provisioning profile found" — Check that your Bundle ID in the workflow matches exactly what's in App Store Connect. Also verify your API key has the Developer role.

"xcodebuild: error: The run destination is not valid" — You're using the wrong macOS runner. Xcode 15.x needs macos-14, Xcode 14.x needs macos-13.

Build succeeds but no TestFlight upload — Check that ExportOptions.plist has method set to app-store and your Team ID is correct.

The Easy Way

If you don't want to write YAML by hand, I built Run Lane — a free visual configurator that generates these workflows for you. Pick iOS + TestFlight, fill in your scheme and Bundle ID, and download a ready-to-use workflow file. No account needed.

It also supports Firebase Distribution and Android (Play Store, Firebase).


Tags: ios, github, cicd, swift

Top comments (0)