iOS CI/CD: Fastlane, GitHub Actions, Xcode Cloud and Safe Releases

Advanced iOS Development: Concurrency, Architecture, Security and Release · Architecture, Delivery and Career · 6 min read · Updated

A release pipeline

  1. A developer opens a pull request with a change.
  2. CI builds the app on a clean machine and runs the tests. A failure blocks the merge.
  3. After merge, a lane increments the build number and sends the build to TestFlight.
  4. The release goes to users in phases. If crashes rise, you can pause it.

Releasing an iOS app by hand means a dozen careful clicks: bump the number, archive, sign, upload, wait, fill in a form. Each click is a chance to forget something, and the person who knows the steps goes on holiday. Continuous integration (CI) builds and tests the app on a server for every change, and continuous delivery (CD) ships it with little or no hand work. Knowing this separates a developer who writes screens from one who can own a release.

What CI should do on every change

  1. Check out the code on a clean machine.
  2. Resolve packages.
  3. Build the app.
  4. Run the unit tests, and the UI tests for the main flows.
  5. Report the result on the pull request, so a broken change cannot be merged by mistake.

Because the machine starts clean, CI also finds the classic *works on my machine* problems, such as a file that is on your computer but was never committed.

Command line first

Every CI tool does the same underlying thing: it runs xcodebuild. If you can run these in a terminal, you can automate them.

xcodebuild test \
  -scheme ExpenseTracker \
  -destination 'platform=iOS Simulator,name=iPhone 16' \
  -resultBundlePath build/TestResults.xcresult

GitHub Actions

If your code is on GitHub, Actions is the easiest place to start. A workflow is a YAML file in .github/workflows/. iOS builds need a macOS runner, which GitHub bills at a higher rate than Linux. Check the current pricing and your free allowance.

name: CI
on:
  pull_request:
  push:
    branches: [main]

jobs:
  test:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4
      - name: Show Xcode version
        run: xcodebuild -version
      - name: Test
        run: |
          xcodebuild test \
            -scheme ExpenseTracker \
            -destination 'platform=iOS Simulator,name=iPhone 16'
  • Pin the Xcode version your project needs if the runner has several, and check the runner image's documentation for how to select it.
  • Cache Swift package downloads to save minutes.
  • Keep secrets, such as keys, in GitHub Secrets and never in the repository.

Fastlane: scripts for releases

Fastlane is a toolbox of release actions written in Ruby, with a simple file called a Fastfile. A *lane* is a named sequence of steps. Teams use it so that *ship a beta* is one command that works the same on every machine.

default_platform(:ios)

platform :ios do
  desc "Run the unit tests"
  lane :test do
    run_tests(scheme: "ExpenseTracker", devices: ["iPhone 16"])
  end

  desc "Build and upload to TestFlight"
  lane :beta do
    api_key = app_store_connect_api_key(
      key_id: ENV["ASC_KEY_ID"],
      issuer_id: ENV["ASC_ISSUER_ID"],
      key_content: ENV["ASC_KEY_CONTENT"]
    )
    increment_build_number(build_number: latest_testflight_build_number(api_key: api_key) + 1)
    build_app(scheme: "ExpenseTracker")
    upload_to_testflight(api_key: api_key)
  end
end
  • run_tests, build_app and upload_to_testflight are the current names for the older scan, gym and pilot actions, and the old names still work. Check Fastlane's documentation for your version.
  • The App Store Connect API key lets automation sign in without your personal password. Create it in App Store Connect, give it the least role it needs, and store its contents in your CI's secrets.
  • Code signing is the part that hurts most. The simplest route is Xcode's automatic signing. For teams, fastlane match keeps the certificates and profiles in a private repository, so every machine uses the same ones.

Xcode Cloud and other services

  • Xcode Cloud is Apple's own CI/CD, set up inside Xcode and App Store Connect. It handles signing for you and sends builds to TestFlight. Check Apple's pages for the current plans and free allowance.
  • Bitrise and similar services are hosted for mobile and give ready-made steps and a visual editor.
  • Self-hosted Mac machines are an option for teams with many builds. Someone has to keep them updated.

The choice usually depends on where the code lives, the budget and how much control the team wants. All of them run the same xcodebuild underneath, so what you learn here transfers.

Versions and build numbers

  • The version (for example 2.4.0) is what people see. It follows a pattern the team agrees on, such as *major.minor.patch*.
  • The build number must go up with every upload to App Store Connect. Automate it, as in the lane above, so nobody has to remember.

Releasing without fear

  • TestFlight first. Every release goes to testers before the public.
  • Phased release. App Store Connect can release an update to a growing share of automatic-update users over several days. If crash reports rise, you can pause it.
  • Feature flags hide new code behind a switch that you control from a server or a config file. You can ship code that is turned off, turn it on for a few people, and turn it off again without a new release. Keep the flag list small and delete old flags.
  • Crash reporting. Xcode Organizer shows crashes from users. Third-party tools such as Firebase Crashlytics or Sentry add more detail and alerts.
  • A hotfix path. Decide in advance how you ship a fix quickly: a branch from the release tag, a short review, the same pipeline. Practice it once when nothing is on fire.
  • Release notes that say what changed, in plain words.

Common mistakes

  • Committing keys or certificates to the repository.
  • A CI that is red for weeks, so everyone ignores it. Fix or remove failing tests quickly.
  • Manual build numbers that clash.
  • Shipping to everyone at once with no way to pause.

Your task

  1. Write the GitHub Actions workflow and open a pull request that runs it. Break a test on purpose and watch the check fail.
  2. Write the test lane in a Fastfile and run it with fastlane test.
  3. Create an App Store Connect API key (only if you have a developer account), store it as secrets and write the beta lane.
  4. Write a one-page release checklist for your app, including the hotfix steps.

How to check it worked

  • A failing test blocks the pull request.
  • fastlane test runs the same tests as CI.
  • You can explain phased release and a feature flag, and when each helps.

Frequently asked questions

What is the difference between CI and CD?

CI (continuous integration) builds and tests every change automatically. CD (continuous delivery) goes further and packages and ships the app, for example to TestFlight, with little or no manual work.

Is Fastlane still used?

Yes, many teams use it to script builds, signing, screenshots and uploads, often inside CI services. Xcode Cloud and other tools cover some of the same ground, and all of them run xcodebuild underneath.

How do I release safely on iOS where review takes time?

Use TestFlight first, phased release, feature flags you can switch from a server, crash reporting, and a hotfix path you have already practised.