| name | ios-ci-cd |
| description | Set up CI/CD pipelines for iOS apps using GitHub Actions, Fastlane, and Xcode Cloud. Use when configuring automated builds, testing, code signing, TestFlight deployment, or App Store release automation. Part of the idea-to-App-Store workflow before publishing. |
iOS CI/CD
Automate building, testing, and deploying iOS applications.
CI/CD Overview
Push → Build → Test → Sign → Deploy → Notify
↓ ↓ ↓ ↓
SwiftLint Unit Match TestFlight
UI App Store
GitHub Actions
Basic Workflow
name: iOS CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
build-and-test:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: Select Xcode
run: sudo xcode-select -s /Applications/Xcode_15.2.app
- name: Show Xcode version
run: xcodebuild -version
- name: Install dependencies
run: |
brew install swiftlint
- name: Run SwiftLint
run: swiftlint --strict
- name: Build
run: |
xcodebuild build \
-workspace MyApp.xcworkspace \
-scheme MyApp \
-destination 'platform=iOS Simulator,name=iPhone 15' \
-configuration Debug \
CODE_SIGN_IDENTITY="" \
CODE_SIGNING_REQUIRED=NO
- name: Run Tests
run: |
xcodebuild test \
-workspace MyApp.xcworkspace \
-scheme MyApp \
-destination 'platform=iOS Simulator,name=iPhone 15' \
-enableCodeCoverage YES \
-resultBundlePath TestResults
- name: Upload Test Results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: TestResults
TestFlight Deployment
name: Deploy to TestFlight
on:
push:
tags:
- 'v*'
jobs:
deploy:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: Select Xcode
run: sudo xcode-select -s /Applications/Xcode_15.2.app
- name: Install Fastlane
run: gem install fastlane
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'
bundler-cache: true
- name: Install Certificates
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }}
MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }}
run: fastlane match appstore --readonly
- name: Build and Upload
env:
APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }}
APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APP_STORE_CONNECT_KEY }}
run: fastlane beta
Fastlane Configuration
Setup
gem install fastlane
cd /path/to/project
fastlane init
Appfile
app_identifier("com.example.myapp")
apple_id("developer@example.com")
team_id("XXXXXXXXXX")
Fastfile
default_platform(:ios)
platform :ios do
before_all do
ensure_git_status_clean
end
desc "Run all tests"
lane :test do
scan(
workspace: "MyApp.xcworkspace",
scheme: "MyApp",
device: "iPhone 15",
code_coverage: true,
output_directory: "./fastlane/test_output"
)
end
desc "Push a new beta build to TestFlight"
lane :beta do
increment_build_number(
build_number: Time.now.strftime("%Y%m%d%H%M")
)
match(type: "appstore", readonly: true)
build_app(
workspace: "MyApp.xcworkspace",
scheme: "MyApp",
configuration: "Release",
export_method: "app-store"
)
upload_to_testflight(
skip_waiting_for_build_processing: true
)
slack(
message: "New beta build uploaded to TestFlight! 🚀",
success: true
)
end
desc "Push a new release to App Store"
lane :release do
test
increment_version_number(
bump_type: "patch"
)
match(type: "appstore", readonly: true)
build_app(
workspace: "MyApp.xcworkspace",
scheme: "MyApp",
configuration: "Release",
export_method: "app-store"
)
upload_to_app_store(
submit_for_review: false,
automatic_release: false,
skip_screenshots: true,
skip_metadata: false
)
add_git_tag(tag: "v#{get_version_number}")
push_to_git_remote
end
desc "Sync certificates and profiles"
lane :sync_signing do
match(type: "development")
match(type: "appstore")
end
desc "Capture screenshots"
lane :screenshots do
capture_screenshots
frame_screenshots(silver: true)
end
error do |lane, exception|
slack(
message: "Build failed: #{exception.message}",
success: false
)
end
end
Code Signing with Match
Setup Match
fastlane match init
fastlane match development
fastlane match appstore
fastlane match appstore --readonly
Matchfile
git_url("git@github.com:yourcompany/certificates.git")
storage_mode("git")
type("appstore")
app_identifier(["com.example.myapp"])
username("developer@example.com")
CI Secrets Required
MATCH_PASSWORD
MATCH_GIT_URL
MATCH_GIT_BASIC_AUTHORIZATION
APP_STORE_CONNECT_KEY_ID
APP_STORE_CONNECT_ISSUER_ID
APP_STORE_CONNECT_KEY
Xcode Cloud
Workflow Configuration
brew install swiftlint
xcodebuild -resolvePackageDependencies
Custom Scripts
swiftlint --strict
echo "Building for environment: $CI_WORKFLOW"
if [ "$CI_XCODEBUILD_EXIT_CODE" -eq 0 ]; then
echo "Build succeeded!"
else
echo "Build failed!"
fi
Xcode Cloud Workflow Settings
Workflow: Beta Releases
├── Start Conditions:
│ └── Tag: v*
├── Environment:
│ ├── Xcode: Latest Release
│ └── macOS: Latest
├── Actions:
│ ├── Build: iOS
│ ├── Test: iOS
│ └── Archive: iOS
└── Post-Actions:
└── Deploy to TestFlight
Build Matrix
Multi-Configuration Testing
jobs:
test:
runs-on: macos-14
strategy:
matrix:
xcode: ['15.0', '15.2']
ios: ['17.0', '17.2']
device: ['iPhone 15', 'iPad Pro (12.9-inch) (6th generation)']
steps:
- uses: actions/checkout@v4
- name: Select Xcode ${{ matrix.xcode }}
run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app
- name: Test
run: |
xcodebuild test \
-workspace MyApp.xcworkspace \
-scheme MyApp \
-destination 'platform=iOS Simulator,name=${{ matrix.device }},OS=${{ matrix.ios }}'
Useful Actions
Caching
- name: Cache Swift packages
uses: actions/cache@v4
with:
path: .build
key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }}
restore-keys: |
${{ runner.os }}-spm-
- name: Cache Ruby gems
uses: actions/cache@v4
with:
path: vendor/bundle
key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
Notifications
- name: Slack Notification
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
fields: repo,message,commit,author,action,eventName,ref,workflow
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
if: always()
Release Checklist
Pre-Release
Deploy Commands
fastlane beta
fastlane release
fastlane beta version:1.2.0
fastlane beta skip_tests:true
Post-Release
Troubleshooting
Common Issues
| Issue | Solution |
|---|
| Code signing failed | Run fastlane match locally first |
| Build timeout | Increase GitHub Actions timeout |
| Simulator not found | Check Xcode version and device name |
| Upload failed | Verify API key permissions |
Debugging Tips
fastlane beta --verbose
fastlane action env
fastlane run validate_provisioning_profile
Resources
See assets/github-actions/ for workflow templates.
See assets/fastlane/ for Fastfile templates.