
Running two Xcode versions on the same CI Mac is a solved problem, but every step has to be deliberate or you will quietly ship an Xcode 16 build linked against the Xcode 15 SDK. This guide installs both versions side-by-side, routes each job to the right one with DEVELOPER_DIR, and adds a short assertion script that stops the job before xcodebuild touches a source file.
for a persistent two-Xcode runner, order a Figment M-series Mac (listed from $95/month; exact chip, RAM, and disk are confirmed in the quote). Start at /mac-hosting/start. If your pipeline is Intel-compatible and you only need Xcode 26 or earlier, Figment's $29 virtual Intel macOS plan (2 vCPU, 8 GiB RAM, 80 GiB disk) can run the toolchain directly — Apple states that to run Xcode on an Intel processor you "use Xcode 26 or earlier" in Porting your macOS apps to Apple silicon. Xcode 27 targeting Intel binaries from an Apple-silicon host needs x86_64 added to ARCHS, per the same page.
Three concrete reasons drive this pattern:
main stays on the released toolchain while a feature branch exercises the next Xcode beta.A single M-series Mac can cover all three roles if DEVELOPER_DIR is set per job. One Mac is cheaper than two, and derived-data caches stay warm.
Rename each bundle before dropping it in /Applications so the installs do not clobber one another:
/Applications/Xcode_15.4.app
/Applications/Xcode_16.2.app
Apple requires you to accept each version's license once. xcode-select -s sets the global developer directory and needs administrator privileges, as documented in Apple's Technical Note TN2339. During runner provisioning, run:
sudo xcode-select -s /Applications/Xcode_15.4.app
sudo xcodebuild -license accept
sudo xcode-select -s /Applications/Xcode_16.2.app
sudo xcodebuild -license accept
This is one-time setup, not a per-job step.
The xcodes CLI installs specific Xcode releases from the command line without the Mac App Store:
brew install xcodesorg/made/xcodes
xcodes install 15.4
mv "/Applications/Xcode.app" "/Applications/Xcode_15.4.app"
xcodes install 16.2
mv "/Applications/Xcode.app" "/Applications/Xcode_16.2.app"
If aria2 is present, xcodes uses it automatically; the project's README notes it can parallelize the download using up to 16 connections. Place a .xcode-version file at the repo root containing the primary version (for example 16.2) so local xcodes select picks the same toolchain developers test against, even though CI still routes with DEVELOPER_DIR.
Disk space. Measure your own installs with du -sh /Applications/Xcode_*.app. Xcode bundles plus a few simulator runtimes commonly land in the tens of gigabytes, so request a quote with your expected free-space need rather than assuming a default disk size.
Inside a job, do not change the global xcode-select default — it is a sudo call that races with every other job on the runner. Set DEVELOPER_DIR in the job environment instead:
export DEVELOPER_DIR=/Applications/Xcode_16.2.app/Contents/Developer
xcodebuild -version
DEVELOPER_DIR overrides the global choice for every tool invoked from that shell — xcodebuild, xcrun, swiftc, swift — and does not leak into other jobs.
Pass DEVELOPER_DIR at the job or step level. GitHub Actions example:
# .github/workflows/build.yml
jobs:
build_xcode15:
runs-on: [self-hosted, macOS]
env:
DEVELOPER_DIR: /Applications/Xcode_15.4.app/Contents/Developer
steps:
- uses: actions/checkout@v4
- name: Assert toolchain
run: bash ci/assert-xcode-version.sh "15.4"
- name: Build
run: xcodebuild build -scheme "$APP_SCHEME" -destination "platform=iOS Simulator,name=iPhone 15"
build_xcode16:
runs-on: [self-hosted, macOS]
env:
DEVELOPER_DIR: /Applications/Xcode_16.2.app/Contents/Developer
steps:
- uses: actions/checkout@v4
- name: Assert toolchain
run: bash ci/assert-xcode-version.sh "16.2"
- name: Build
run: xcodebuild build -scheme "$APP_SCHEME" -destination "platform=iOS Simulator,name=iPhone 16"
Set APP_SCHEME from your project's real scheme — do not hard-code a placeholder. If your CI uses runner tags, dedicate xcode-15 and xcode-16 labels instead; on one shared runner, DEVELOPER_DIR is simpler.
Keep ci/assert-xcode-version.sh in source control and call it before any xcodebuild step. It reads the active version and exits non-zero if it does not match the expected argument:
#!/usr/bin/env bash
# Usage: bash ci/assert-xcode-version.sh "16.2"
set -euo pipefail
EXPECTED="${1:?expected Xcode version required, e.g. 16.2}"
ACTIVE=$(xcodebuild -version | awk '/^Xcode /{print $2}')
if [[ "${ACTIVE}" != "${EXPECTED}" ]]; then
echo "ERROR: expected Xcode ${EXPECTED}, active ${ACTIVE}"
echo " DEVELOPER_DIR=${DEVELOPER_DIR:-<unset>}"
exit 1
fi
echo "OK: Xcode ${ACTIVE} active"
echo " SDK: $(xcrun --show-sdk-path)"
set -euo pipefail guarantees the job stops on the first failure — the script's exit status is the signal, nothing counts a failed build as a pass.
Beyond xcodebuild -version, keep a short health step on every push to main:
xcodebuild -version
xcrun --find xcodebuild
xcrun --show-sdk-path
xcrun simctl list runtimes
A few-second step catches a corrupted DEVELOPER_DIR before it wastes a 10-minute build.
| Need | Figment plan | Monthly list |
|---|---|---|
| Intel-only toolchain, Xcode 26 or earlier, small team | Virtual Intel macOS | $29 |
| ARM targets, two Xcode versions on one runner | M-series (request) | from $95 |
| Fully managed CI on one M-series Mac: setup, backups, monitoring, 30 min/mo support | Managed Build Mac | $249 |
A twelve-month comparison against one competitor, so you can judge entry price honestly: Figment M-series at $95/month lists at $1,140 per year; CloudMac's pricing page shows an M4 (16 GB RAM, 256 GB SSD) at $99.99/month, and cloudmac.org advertises $999/year for the same tier. That annual rate works out to $83.25 effective monthly and sits $141 below 12 × $95. Figment's $95 entry is an unspecified M-series — chip and RAM are confirmed in your quote, not equivalent to a listed M4. Where Figment earns the budget is the Managed Build Mac tier at $249/month: the two-Xcode DEVELOPER_DIR wiring, backups, and monitoring described above are the work included. If you only need an unmanaged M4, read both pricing pages before you decide.
/Applications with distinct namesxcodebuild -license acceptDEVELOPER_DIR is set at the job level, not via global sudo xcode-select -sassert-xcode-version.sh runs before the first xcodebuild call in every job.xcode-version at the repo root matches the primary CI toolchainReady to provision the runner? Review plans at /mac-hosting and open a configuration quote at /mac-hosting/start.
Related guides: