Skip to content
Latchkey

CI/CD for iOS Apps with GitHub Actions

iOS builds need Xcode, and Xcode needs a macOS runner -- here is a clean pipeline that gets it right.

Continuous integration for an iOS app means compiling with Xcode, running your XCTest suite, and producing a build you can archive. Apple toolchains only run on macOS, so the build job must target a macOS runner. This recipe gives you a working workflow plus the cost trade-offs that matter for Apple builds.

What the pipeline does

  • Checks out the repository on a macOS runner.
  • Selects a specific Xcode version for reproducibility.
  • Runs the unit-test suite with xcodebuild against a simulator.
  • Builds the app to confirm a release configuration compiles.

The workflow

Pin the Xcode version explicitly so a runner image update does not silently change your toolchain.

.github/workflows/ios-ci.yml
name: iOS CI
on: [push, pull_request]
jobs:
  build-test:
    runs-on: macos-14
    steps:
      - uses: actions/checkout@v4
      - name: Select Xcode
        run: sudo xcode-select -s /Applications/Xcode_15.4.app
      - name: Run unit tests
        run: |
          xcodebuild test \
            -scheme MyApp \
            -destination "platform=iOS Simulator,name=iPhone 15,OS=latest" \
            CODE_SIGNING_ALLOWED=NO
      - name: Build release
        run: |
          xcodebuild build \
            -scheme MyApp \
            -configuration Release \
            -destination "generic/platform=iOS" \
            CODE_SIGNING_ALLOWED=NO

Notes for this platform

macOS runners are the most expensive class on every CI provider, and GitHub-hosted macOS minutes bill at roughly ten times the Linux rate. Keep the macOS job tight -- only the steps that genuinely need Xcode -- and run linting, formatting, and dependency checks on a cheaper Linux job. On managed runners, transient simulator boot failures and flaky network installs are retried automatically instead of failing the whole run, which keeps Apple-toolchain pipelines from wasting costly macOS minutes on infrastructure noise.

Making this reliable in CI

  • Pin every tool version. An unpinned toolchain turns a runner image update into a build break on unchanged code.
  • Cache what is expensive to produce, and key the cache to the tool version so a restore across a version boundary cannot poison the build.
  • Assert on the produced artifact rather than on the command succeeding.
  • Set an explicit timeout so a hung step fails fast instead of consuming the whole job budget.

Key takeaways

  • iOS builds require a macOS runner because Xcode does not run on Linux.
  • Pin the Xcode version with xcode-select so runner image updates do not change your toolchain.
  • Push non-Xcode work (lint, deps) to cheap Linux jobs to limit costly macOS minutes.

Frequently asked questions

CI/CD for iOS Apps with GitHub Actions?
Continuous integration for an iOS app means compiling with Xcode, running your XCTest suite, and producing a build you can archive. Apple toolchains only run on macOS, so the build job must target a macOS runner. This recipe gives you a working workflow plus the cost trade-offs that matter for Apple builds.
The workflow?
Pin the Xcode version explicitly so a runner image update does not silently change your toolchain.
Notes for this platform?
macOS runners are the most expensive class on every CI provider, and GitHub-hosted macOS minutes bill at roughly ten times the Linux rate. Keep the macOS job tight -- only the steps that genuinely need Xcode -- and run linting, formatting, and dependency checks on a cheaper Linux job.

Related guides

References

Run this faster and cheaper on Latchkey managed runners - self-healing included. Start free → 30-day trial · No credit card