Engineering Guides

Validate Shared Xcode Schemes for Cloud Mac CI

Validate Shared Xcode Schemes for Cloud Mac CI

After a team checks out a project on a cloud Mac, the most commonly underestimated failure is not a compilation error. It is the pipeline failing to find the expected scheme—or running different targets from a scheme with the same name in the remote environment. Local Xcode reads user-specific settings from the developer directory, while a clean CI workspace only sees files committed to the repository. To keep the build entry point stable, schemes must be version-controlled and validated just like scripts and dependency lockfiles.

Treat the Scheme as a CI Interface

A scheme invoked by a pipeline defines at least four things: which targets to build, which build configuration to use, which test bundles to run, and which configuration to apply when archiving. Seeing its name in the Xcode menu does not mean this information has been committed.

First, check whether the shared file exists:

find . \( -path "*/xcshareddata/xcschemes/*.xcscheme" \
  -o -path "*/xcuserdata/*/xcschemes/*.xcscheme" \) -print

A scheme under xcuserdata is user-specific and cannot serve as a CI entry point. Enable Shared in Xcode’s Manage Schemes window, then confirm that the file appears at one of these paths:

App.xcodeproj/xcshareddata/xcschemes/App.xcscheme
App.xcworkspace/xcshareddata/xcschemes/App.xcscheme

Next, use git status and git check-ignore -v to confirm that the file is tracked and has not been accidentally excluded by an overly broad *.xcuser* or xcshareddata ignore rule.

“Visible locally” only means the configuration exists in the current user’s directory. “Available to CI” must be demonstrated by command-line results from a clean checkout.

Pin the Workspace, Scheme, and Configuration

The pipeline should not rely on Xcode to select a project automatically. When CocoaPods, aggregate projects, or local packages are involved, opening the .xcodeproj can produce different results from opening the .xcworkspace. Define all three entry-point values explicitly as CI variables:

WORKSPACE="App.xcworkspace"
SCHEME="App"
CONFIGURATION="Release"

xcodebuild \
  -workspace "$WORKSPACE" \
  -scheme "$SCHEME" \
  -configuration "$CONFIGURATION" \
  -list

The output from -list should include the expected scheme. Then use JSON output for machine validation instead of relying solely on someone reading the logs:

xcodebuild -workspace "$WORKSPACE" -list -json > xcode-list.json
python3 - <<'PY'
import json
data = json.load(open("xcode-list.json"))
schemes = data.get("workspace", {}).get("schemes", [])
if "App" not in schemes:
    raise SystemExit("Required Xcode scheme is missing")
PY

If the repository keeps separate entry points for development, staging, and production, their names should describe their purpose—for example, App-Dev, App-Staging, and App-Release. Do not let a script select the first scheme based on list order; adding a dependency can change that order.

Validate Build, Test, and Archive Separately

Sharing a scheme only makes it visible; it does not prove that its actions are correct. Split validation into three layers.

Inspect the Resolved Build Settings

First, export the final settings. Pay particular attention to PRODUCT_BUNDLE_IDENTIFIER, CONFIGURATION, SDKROOT, SUPPORTED_PLATFORMS, and the output directories:

xcodebuild \
  -workspace "$WORKSPACE" \
  -scheme "$SCHEME" \
  -configuration "$CONFIGURATION" \
  -showBuildSettings > build-settings.txt

grep -E "PRODUCT_BUNDLE_IDENTIFIER|CONFIGURATION =|SDKROOT|SUPPORTED_PLATFORMS" \
  build-settings.txt

These values reflect the merged result of the scheme, project settings, and command-line arguments, so they are closer to the actual build than a direct inspection of project.pbxproj.

Validate the Test Entry Point

During the test stage, build the testable products before running the tests. This separates a broken compilation entry point from a test execution failure:

xcodebuild \
  -workspace "$WORKSPACE" \
  -scheme "$SCHEME" \
  -destination "platform=iOS Simulator,name=iPhone 16" \
  build-for-testing

The simulator name must come from xcrun simctl list devices available on the current machine. Do not hard-code a device that exists only on a developer’s computer. If the team needs to pin a runtime, record both the Xcode version and the target OS version.

Validate the Archive Action Separately

Archive may use a different configuration from Run or Test. First verify the ArchiveAction configuration in the .xcscheme, then perform a real archive in a controlled job:

xcodebuild \
  -workspace "$WORKSPACE" \
  -scheme "$SCHEME" \
  -configuration Release \
  -destination "generic/platform=iOS" \
  -archivePath "$PWD/artifacts/App.xcarchive" \
  archive

Do not treat a successful regular build as a substitute for archive validation. Resource processing, script phases, and archive settings often reveal problems only at this stage.

Clear Local State and Validate Again

The most reliable check is not deleting DerivedData, but cloning the repository again into a new directory. This exposes uncommitted schemes, local symbolic links, untracked configuration, and implicit dependencies at the same time:

ROOT="$(mktemp -d)"
git clone --no-local . "$ROOT/repo"
cd "$ROOT/repo"
git submodule update --init --recursive
xcodebuild -workspace App.xcworkspace -scheme App -list

If the project depends on a generation step, run its command before the build and verify that the generated output is reproducible. Do not put “open the project once in Xcode” in the runbook. Such a step cannot be part of an unattended job and can conceal missing files.

Clean-room validation should cover at least the following:

Check Passing criterion
Scheme visibility xcodebuild -list -json returns the specified name
File ownership The scheme is under xcshareddata and tracked by Git
Build configuration Command-line-resolved settings match the pipeline’s expectations
Test entry point build-for-testing produces testable products
Archive entry point A generic iOS destination produces an .xcarchive
Non-interactive execution No need to open Xcode or read from a user directory

Block Entry-Point Drift Before Merge

Schemes are XML files, and merge conflicts are often handled like ordinary text conflicts. The result may preserve the scheme name while losing a Testable or BuildActionEntry. Add lightweight checks to merge requests: verify that the shared file exists, reject committed xcuserdata, and run xcodebuild -list -json.

Also review the scheme’s Pre-actions and Post-actions. Remote execution will still fail if a script refers to absolute paths, interactive shell configuration, or tools installed only on one developer’s machine. Scripts should locate files through build variables such as SRCROOT and exit with a clear error when a dependency is missing.

The final acceptance criterion is straightforward: starting from an empty directory, obtain the repository and provide only the documented Xcode version and commands. The job must then be able to enumerate schemes, resolve settings, build testable products, and complete an archive. Only at that point is the cloud Mac on G-Mini executing the entry point defined by the repository rather than reproducing the accidental state of a developer’s computer.

Frequently asked questions

Why can Xcode run locally while cloud Mac CI cannot find the scheme?

The scheme is usually stored under xcuserdata instead of xcshareddata/xcschemes. A clean CI checkout has no access to developer-specific Xcode data, so the shared scheme file must be committed.

Does sharing an Xcode scheme prove that Archive will work?

No. Sharing only makes the scheme visible. Validate the Release configuration, archive target, pre-actions, and environment assumptions, then complete at least one archive from a clean checkout.

Exclusive physical nodes

Cloud Mac for your next build queue

Compare two M4 configurations across five nodes, then choose daily, weekly, monthly, or quarterly rental based on your workflow.

Choose a Cloud Mac plan