Release engineering: TestFlight, CI and App Store Connect
Certificates and provisioning explained, automated builds with fastlane and Xcode Cloud, TestFlight distribution, phased release and turning crash reports into fixes.
Signing and provisioning
| Piece | What it is | Who creates it |
|---|---|---|
| Certificate | Identity that proves the build came from you | Apple, from a CSR in your keychain |
| App ID | Bundle identifier and enabled capabilities | Developer portal or automatic signing |
| Provisioning profile | Binds certificate, App ID and devices | Generated, ideally automatically |
| Export options | How the archive is packaged for a destination | Your build configuration |
# archive and export exactly as CI would, from the command line
xcodebuild archive \
-project App.xcodeproj \
-scheme App -configuration Release \
-destination "generic/platform=iOS" \
-archivePath build/App.xcarchive
xcodebuild -exportArchive \
-archivePath build/App.xcarchive \
-exportOptionsPlist ExportOptions.plist \
-exportPath build/ipaFor a team, use App Store Connect API keys rather than an Apple ID password in CI. The key can be scoped, rotated and revoked without changing anyone's personal credentials.
fastlane and Xcode Cloud
# fastlane/Fastfile
default_platform(:ios)
platform :ios do
desc "Run tests then upload a beta build"
lane :beta do
increment_build_number(
build_number: ENV["GITHUB_RUN_NUMBER"] || "1"
)
run_tests(scheme: "App")
build_app(
scheme: "App",
export_method: "app-store",
output_directory: "build"
)
upload_to_testflight(
skip_waiting_for_build_processing: true,
changelog: "Automated beta build"
)
end
end# .github/workflows/ios.yml
name: ios
on:
push:
branches: [main]
jobs:
beta:
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- name: Select Xcode
run: sudo xcode-select -s /Applications/Xcode_16.app
- name: Cache SPM
uses: actions/cache@v4
with:
path: ~/Library/Caches/org.swift.swiftpm
key: spm-${{ hashFiles('**/Package.resolved') }}
- run: bundle exec fastlane beta
env:
APP_STORE_CONNECT_KEY_ID: ${{ secrets.ASC_KEY_ID }}
APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
APP_STORE_CONNECT_KEY_CONTENT: ${{ secrets.ASC_KEY_CONTENT }}⚠️
Never store signing certificates or API keys as plain repository secrets you cannot audit. Use a scoped App Store Connect API key, restrict who can trigger the release job, and require a manual approval before the production upload step.
TestFlight and the store
- Internal testers get a build within minutes; external testers need a review that can take a day, so plan the first beta a week ahead.
- Phased release rolls a version out over seven days and can be paused, which limits the blast radius of a bad build.
- Use TestFlight feedback screenshots and crash reports as the first triage signal; most issues are visible in the hour after upload.
- Automate the release notes: a build number and commit list is more useful than "bug fixes".
- Tag the exact commit that produced a shipped build so a hotfix can branch from it.
Before submitting, walk the review checklist that rejects most apps: accurate privacy manifest, working demo account, no placeholder content, and an explanation for every permission the app requests.
FAQ
What is the difference between build number and version number?
The version (marketing) is what users see and must increase for a new App Store release. The build number distinguishes uploads of the same version and must be unique per upload — derive it from your CI run number.
Should I use Xcode Cloud or my own CI?
Xcode Cloud is tightly integrated and handles signing, but bills in compute hours and is Apple-only. A self-hosted or hosted runner with fastlane is more portable if you also build Android or web from the same repository.
Related
Testing, debugging and Instruments Security, privacy and data protection
Last refreshed 2026-09-18.