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

PieceWhat it isWho creates it
CertificateIdentity that proves the build came from youApple, from a CSR in your keychain
App IDBundle identifier and enabled capabilitiesDeveloper portal or automatic signing
Provisioning profileBinds certificate, App ID and devicesGenerated, ideally automatically
Export optionsHow the archive is packaged for a destinationYour 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/ipa

For 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.

Testing, debugging and Instruments Security, privacy and data protection

Last refreshed 2026-09-18.