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
- Check out the code on a clean machine.
- Resolve packages.
- Build the app.
- Run the unit tests, and the UI tests for the main flows.
- 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_appandupload_to_testflightare the current names for the olderscan,gymandpilotactions, 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 matchkeeps 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
- Write the GitHub Actions workflow and open a pull request that runs it. Break a test on purpose and watch the check fail.
- Write the
testlane in aFastfileand run it withfastlane test. - Create an App Store Connect API key (only if you have a developer account), store it as secrets and write the
betalane. - 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 testruns the same tests as CI.- You can explain phased release and a feature flag, and when each helps.