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.
- Go to App Store Connect → Users and Access → Keys
- Click Generate API Key
- Name it "GitHub Actions CI" and give it Developer role
- Download the
.p8file (you can only download it once!) - 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.
- Open Keychain Access on your Mac
- Find your "Apple Distribution" certificate
- Right-click → Export → save as
.p12with a password - Base64-encode it:
base64 -i Certificates.p12 | pbcopy
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 }}
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>
Step 6: Push and Watch
git add .
git commit -m "Add TestFlight CI/CD workflow"
git push origin main
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)