Figment.so
Mac hostingWebsite hostingGuidesHow to usePricing

Automate watchOS App Builds in a CI Pipeline

A watchOS release is really two binaries: the iOS companion and the Watch extension, packaged inside one .ipa. CI has to compile both, run the Watch extension test bundle against a paired simulator, and exit non-zero on any failure. Two things trip most pipelines up: the destination syntax and losing xcodebuild's exit code when output is piped through a formatter. Everything below is the minimum correct wiring, with a worked Figment cost example at the end.

The Paired Build Model

Since watchOS 7 (Xcode 12) Watch apps can ship independently, and Xcode 14 made independent the default for new projects. Most shipping apps still carry an iOS companion. When both targets share one scheme, xcodebuild compiles the Watch extension as a dependency of the iOS app, so you do not need a second invocation.

Compile once, then run tests from the cached artefact (Apple's recommended CI pattern):

xcodebuild \
  -workspace YourApp.xcworkspace \
  -scheme "YourApp" \
  -configuration Debug \
  -destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \
  build-for-testing

build-for-testing writes an .xctestrun bundle without running tests. A later test-without-building step reuses that artefact, so compile time is paid once per commit regardless of how many destinations you fan out to.

Pinning the Watch Simulator Destination

List what the runner has, then pick a UDID. Name-based destinations break every time Xcode or a runtime updates:

xcrun simctl list devices available | grep -E "Watch|iPhone"
WATCH_UDID=$(xcrun simctl list devices available -j \
  | python3 -c "
import sys, json
devs = json.load(sys.stdin)['devices']
print(next(
  d['udid']
  for runtimes in devs.values()
  for d in runtimes
  if 'Apple Watch' in d['name'] and d['state'] != 'unavailable'
))") || { echo "No watchOS simulator found"; exit 1; }

Pass -destination "id=${WATCH_UDID}" to every subsequent xcodebuild call. Without a valid destination, xcodebuild exits before any test executes, and a sloppy pipe-through-formatter pipeline can report that failure as success.

Running the Watch Extension Tests Headlessly

Boot the simulator before invoking xcodebuild test; a cold boot inside the test command can blow past CI timeouts. The script below is the artefact to copy into your repo. The crucial line is PIPESTATUS[0]: it preserves xcodebuild's real exit code when stdout is piped through xcpretty.

#!/usr/bin/env bash
# ci-watchos-test.sh
# Required: XCODE_SCHEME, XCODE_WORKSPACE
# Optional: WATCH_RESULT_BUNDLE (default build/watchos-tests.xcresult)
set -euo pipefail

SCHEME="${XCODE_SCHEME:?Set XCODE_SCHEME in CI environment}"
WORKSPACE="${XCODE_WORKSPACE:?Set XCODE_WORKSPACE in CI environment}"
RESULT_BUNDLE="${WATCH_RESULT_BUNDLE:-build/watchos-tests.xcresult}"

echo "==> Locating watchOS simulator"
WATCH_UDID=$(xcrun simctl list devices available -j \
  | python3 -c "
import sys, json
devs = json.load(sys.stdin)['devices']
print(next(
  d['udid']
  for runtimes in devs.values()
  for d in runtimes
  if 'Apple Watch' in d['name'] and d['state'] != 'unavailable'
))") || { echo "ERROR: no available watchOS simulator" >&2; exit 1; }
echo "Using simulator: ${WATCH_UDID}"

echo "==> Booting simulator"
xcrun simctl bootstatus "${WATCH_UDID}" -b

echo "==> Running WatchKit Extension tests"
set +e
xcodebuild test \
  -workspace "${WORKSPACE}" \
  -scheme "${SCHEME}" \
  -destination "id=${WATCH_UDID}" \
  -resultBundlePath "${RESULT_BUNDLE}" \
  -enableCodeCoverage YES \
  2>&1 | xcpretty --color
TEST_EXIT=${PIPESTATUS[0]}
set -e

if [ "${TEST_EXIT}" -ne 0 ]; then
  echo "ERROR: xcodebuild exited ${TEST_EXIT} - inspect ${RESULT_BUNDLE}" >&2
  exit "${TEST_EXIT}"
fi

echo "==> All tests passed"

Treat this as an unexecuted example to adapt - run it once locally against your real scheme name before trusting the signal in CI. Without PIPESTATUS[0], a successful formatter exit will overwrite a failed test exit and the pipeline reports green on a broken build.

Common Failure: Missing Companion Target in the Archive

When archiving for TestFlight, xcodebuild archive produces a .xcarchive. If the scheme's Archive action excludes the Watch app target, the archive contains only the iOS binary and TestFlight rejects the upload because the Watch entitlement references a bundle that is not present.

Check the scheme list, then open Product > Scheme > Edit Scheme > Archive in Xcode and confirm the WatchKit App target is listed with Build checked:

xcodebuild -list -workspace YourApp.xcworkspace

No flag compensates for a misconfigured scheme - fix the .xcscheme file in source control and commit it.

Uploading the iOS + Watch IPA

xcodebuild -exportArchive produces a single .ipa. The Watch app is embedded at Payload/YourApp.app/Watch/WatchApp.app, so TestFlight accepts one upload for both platforms. With Fastlane:

fastlane pilot upload \
  --ipa "build/export/YourApp.ipa" \
  --api_key_path "${API_KEY_PATH}"

API_KEY_PATH should point to a file written at runtime from a CI-managed secret - decoded into a job-scoped temp directory, never committed. See the iOS CI secrets guide for a safe decode step. Update only your own CI, artifact store and firewall allowlists; there is no documented App Store Connect ingress IP range to allowlist.

Xcode Version Pinning and Hardware Choice

The watchOS SDK ships inside Xcode. If a runner auto-updates Xcode, simulator runtime names change and destinations break. Pin the toolchain and log the version on every run:

sudo xcode-select --switch /Applications/Xcode_26.1.app/Contents/Developer
xcodebuild -version

For the hardware under the runner, Apple states plainly: Intel Macs must use Xcode 26 or an earlier release (Apple developer docs). visionOS tooling needs Apple silicon regardless, so match the host to the toolchain your project is pinned on.

Figment plans for this pipeline

  • $29 virtual Intel macOS - a virtual Intel macOS environment accessed from the browser console. It runs supported Intel Xcode toolchains (Xcode 26 and earlier) and is the cheapest way to run the build-for-testing step against the iOS + watchOS pair while you evaluate. No GPU acceleration and no inbound SSH; agents reach out to your CI service.
  • Physical i3 at $49/mo (request-based) - adds a physical Intel host for workloads that need bare-metal timing. Request-based until supply is confirmed.
  • M-series from $95/mo (request-based) - needed if you must pin Xcode 27 or later, build visionOS targets, or compile arm64-only dependencies. The exact chip generation, RAM and disk are confirmed in the quote; $95 is an entry price, not a specific M4 configuration.
  • Managed Build Mac $249/mo - one M-series Mac, CI setup, backups, monitoring, 30 minutes of monthly support. This is the recommendation if you want Figment to wire the watchOS pipeline above (scheme audit, simulator pinning, Fastlane upload step).

Worked cost example for a one-runner watchOS pipeline

Compare like-for-like entry prices against the primary vendor pages. CloudMac publishes $99.99/month for a Mac mini M4/16 GB/256 GB and a $999/year option on cloudmac.org - that annual rate is $83.25/month effective, which is lower than 12 x $95 = $1,140 for Figment's M-series entry. CloudMac's 256 GB row shows "Reserve now"; the 512 GB and 1 TB rows show "Fully reserved". MacStadium lists M2.S at $109, M4.S at $149 and M4.M at $249, with support and a 99.99% uptime claim on that page.

If you only need to evaluate the pipeline:

  • 1 month on Figment's $29 virtual Intel macOS to wire build-for-testing and the paired-destination script: $29.
  • Move the same pipeline to the M-series when you pin Xcode 27 or add visionOS targets: $95/mo entry (specific chip confirmed in quote).

If you want Figment to run the watchOS wiring for you, Managed Build Mac at $249/mo bundles the M-series host, CI setup, backups, monitoring and 30 minutes of monthly support. Figment's managed CI work is the material difference against hardware-first vendors - not the raw monthly rate.

Request a configuration at /mac-hosting or start a build host now at /mac-hosting/start.


Related guides:


Subscribe for Figma design and website publishing guides.


Figment.so

Contact

Twitter

Website pricing

Compute index

About

Privacy

Terms