How Do I Select an Xcode Version in GitHub Actions?

Pick the runner image that carries the Xcode version you want, then point the job at that toolchain with DEVELOPER_DIR or xcode-select and log xcodebuild.

Selecting an Xcode version in GitHub Actions is two decisions. Pick the runner image that carries the Xcode version you want, because the image decides which toolchains exist on disk, then point the job at that toolchain by setting DEVELOPER_DIR at job level or running sudo xcode-select -s on the bundle path, and print xcodebuild -version as the first step so the log records which toolchain compiled the build.

This page covers the label-to-Xcode mapping you choose from, the workflow syntax for both selection mechanisms, and the failure that wastes the most macOS minutes: pinning a version the image does not carry.

Answer

The image is the menu and the job makes the pick. runs-on selects the image, so it decides which Xcode bundles are under /Applications before any step runs. Nothing inside the workflow installs a new Xcode in a reasonable amount of time, so a version that is absent from the image is a label change rather than a step change.

WarpBuild provides Linux x64, Linux ARM64, macOS, and Windows runners. The macOS line is ARM64 and covers three images across five labels. Every row below comes from the cloud runners documentation, checked on 2026-08-13.

Runner labelmacOS imagevCPU / memoryXcode versions on the imagePer minute
warp-macos-26-arm64-6xmacOS 266 / 22GBUpstream GitHub macOS 26 set, plus Xcode 27.0 (build 27A5194q)$0.08
warp-macos-26-arm64-12xmacOS 2612 / 44GBUpstream GitHub macOS 26 set, plus Xcode 27.0 (build 27A5194q)$0.16
warp-macos-15-arm64-6xmacOS 156 / 22GBUpstream GitHub macOS 15 set$0.08
warp-macos-15-arm64-12xmacOS 1512 / 44GBUpstream GitHub macOS 15 set$0.16
warp-macos-14-arm64-6xmacOS 146 / 22GBThe Xcode 15 line, with 15.4 as the default$0.08

The published bundle inventory for each image lives in GitHub's runner-images repository: macOS 14 ARM64, macOS 15 ARM64, and macOS 26 ARM64. WarpBuild images carry the same tooling as the matching GitHub-hosted image, and the readme link for each one is collected in the preinstalled software documentation. Those readmes are regenerated on every image release, so an exact patch version such as 16.2 belongs there rather than in a page that refreshes on a fixed cadence.

Two labels are aliases. warp-macos-latest-arm64-6x and warp-macos-latest-arm64-12x resolve to the macOS 15 image, in sync with GitHub's macos-latest tag, so an Xcode-sensitive job should name the versioned label. macOS 13 runners were removed on June 8, 2026, and a workflow still naming a macOS 13 label queues without a match (cloud runners documentation).

With the label chosen, this is the selection inside the job:

name: ios-pr

on:
  pull_request:
    branches: [main]

jobs:
  build-and-test:
    runs-on: warp-macos-26-arm64-6x
    env:
      DEVELOPER_DIR: /Applications/Xcode_27.0.app/Contents/Developer
    steps:
      - name: Guard the toolchain
        run: |
          ls -1 /Applications | grep -i '^Xcode'
          xcodebuild -version
          xcrun simctl list runtimes | grep iOS

      - uses: actions/checkout@v4

      - name: Resolve dependencies
        run: xcodebuild -resolvePackageDependencies -project App.xcodeproj

      - name: Build and test
        run: |
          xcodebuild test \
            -project App.xcodeproj \
            -scheme App \
            -destination 'platform=iOS Simulator,name=iPhone 17,OS=27.0' \
            -resultBundlePath TestResults.xcresult

      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: xcresult
          path: TestResults.xcresult

DEVELOPER_DIR set under jobs.<id>.env applies to every step in that job, including xcodebuild, swift, and every xcrun call, and job-level env is standard GitHub Actions syntax (GitHub Actions workflow syntax). The guard step runs before checkout so the toolchain question is settled while the job has done no real work.

Detail

Three mechanisms, one decision

MechanismScopeNeeds sudoWhat the log shows
DEVELOPER_DIR under job-level envevery step in the jobnothe resolved version from xcodebuild -version
sudo xcode-select -s <path> as a stepthe machine for the rest of the jobyesthe switch in that step, inherited by later steps
maxim-lobanov/setup-xcode actionsteps after the action runsnothe version the action selected, from its own output

DEVELOPER_DIR is the smallest change and the easiest to read in review, because the path sits next to runs-on in the same job block. sudo xcode-select -s /Applications/Xcode_27.0.app/Contents/Developer is the right choice when a later step shells out to a tool that ignores the environment and reads the active developer directory instead. The setup-xcode action is a public OSS action that accepts a version spec such as 16.x and resolves it against the bundles present on the image, which suits a matrix where each leg names a version range rather than an exact bundle.

Whichever mechanism you pick, use one of them per job. A DEVELOPER_DIR value and an xcode-select call that disagree resolve differently depending on which tool reads which, and the resulting mismatch is invisible until a compile behaves differently from the local build.

Bundle paths are set by the image

Xcode installs on the runner images live at /Applications/Xcode_<version>.app, with /Applications/Xcode.app as the image default. The version fragment matches the bundle list in the image readme, so Xcode_27.0.app on macOS 26 and the versioned bundles on macOS 15 come straight from that inventory. Run ls -1 /Applications | grep -i '^Xcode' once on the label you plan to use and pin the exact name that prints.

The macOS 26 labels are the one place where the WarpBuild image adds a bundle. warp-macos-26-arm64-6x and warp-macos-26-arm64-12x ship Xcode 27.0 (build 27A5194q) on top of the Xcode versions in the upstream GitHub macOS 26 image, with the iOS 27.0 (24A5355p), tvOS 27.0 (24J5289o), watchOS 27.0 (24R5289n), and visionOS 27.0 (24M5291p) simulator runtimes bundled with it (macOS 26 tooling). That addition exists while GitHub's upstream macOS 27 runner image is in beta, and a dedicated macOS 27 image follows once that image is released, so a job pinning the Xcode 27.0 path takes one edit at that switch. The per-image matrix is on the Xcode image reference.

macOS 14 is the narrowest of the three. GitHub removed Xcode 14 and Xcode 16 from its macOS 14 images on November 4, 2024, leaving the Xcode 15 line (actions/runner-images issue 10703), and the default on that image points at 15.4 to match GitHub-hosted behavior (March 2025 changelog). A job that needs Xcode 16 or later moves up an image.

The failure mode: pinning a version the image does not carry

This is the common way an Xcode pin goes wrong. The workflow names Xcode_16.4.app, the image carries a different patch release, and the path resolves to nothing. xcode-select reports an invalid developer directory for that path, and any xcodebuild or xcrun call that inherits the bad DEVELOPER_DIR fails the same way. A simulator pin fails in a related manner: -destination with OS=27.0 finds no matching device when the runtime is absent, even though the compile succeeded.

The guard step is what keeps that cheap. ls -1 /Applications | grep -i '^Xcode', xcodebuild -version, and xcrun simctl list runtimes run in the first ten seconds of the job, before checkout, before dependency resolution, and before a compile that would otherwise have burned macOS minutes at $0.08 per minute on a 6 vCPU label (cloud runners documentation, checked on 2026-08-13). The log then carries three facts: the bundles present, the toolchain selected, and the runtimes installed. A red job with those three lines in it is a two-minute fix. A red job forty minutes into a build with a confusing compiler error is an afternoon.

Two habits make the guard hold over time. Keep the guard step in the workflow permanently rather than deleting it after the first green run, since image updates are the event that invalidates a pin, and runner image changes are published in the WarpBuild changelog. Second, when the log leaves you guessing about the runner state, the Action Debugger opens an SSH session into a running workflow so you can list /Applications yourself (what is WarpBuild).

What is the difference between DEVELOPER_DIR and xcode-select?

DEVELOPER_DIR is an environment variable read by xcodebuild, swift, and xcrun, so setting it at job level scopes the choice to that job with no sudo. sudo xcode-select -s changes the active developer directory on the machine for the rest of the job, which affects every later step whether or not it wanted the switch. The pipeline-wide pattern, including matrix legs and release jobs, is in the Xcode version pinning guide.

Which Xcode versions can I select on a given runner label?

The ones installed on the image that label boots. Each macOS image carries the Xcode bundles of the upstream GitHub image it is built from, and the macOS 26 labels additionally ship Xcode 27.0 (build 27A5194q) with the iOS, tvOS, watchOS, and visionOS 27.0 simulator runtimes. The per-image matrix is on the Xcode image reference, and the version-by-version breakdown is in which Xcode versions GitHub Actions runners carry.

Does warp-macos-latest-arm64-6x give me the newest Xcode?

No. The latest alias resolves to the macOS 15 image, in sync with GitHub's macos-latest tag, so a job on that alias gets the macOS 15 Xcode set. Pin warp-macos-26-arm64-6x when the job needs the macOS 26 image and Xcode 27.0. Every label with its image, size, and rate is on the macOS runner hub.

Pick a label on the macOS runner hub, confirm the Xcode set for that image on the Xcode image reference, then add the DEVELOPER_DIR line and the guard step to the job.

Start with $10 in free credits

Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.