Skip to content

CI Integration

This page covers running AutoMobile XCTestRunner tests in GitHub Actions using a macOS runner with a built-in iOS Simulator. The automobile-tests job shown here is the reference workflow used in the ios-build repository.

Overview

AutoMobile tests need:

  1. A pre-built .xctestrun containing the YourAppAutoMobileTests bundle
  2. A booted iOS Simulator on the runner
  3. A running AutoMobile daemon reachable over the Unix socket
  4. The CtrlProxy iOS app installed in the simulator

The sections below walk through each step.

No cloud device service required

Unlike Android — which uses emulator.wtf to provision managed cloud emulators — iOS AutoMobile tests run on the macOS runner’s built-in iOS Simulator. No external device service or additional secrets are needed.

Job structure

The reference workflow splits testing into two independent jobs that share the same build artifact:

build-for-testing ──┬──► simulator-tests (unit tests, skips AutoMobile bundle) └──► automobile-tests (AutoMobile tests only)

Both jobs download the same .xctestrun artifact from build-for-testing and run xcodebuild test-without-building with different filtering flags, so there is no duplicate compilation.

Full workflow job

```yaml automobile-tests: name: “AutoMobile XCTestRunner Tests” runs-on: macos-26 needs: [build-for-testing] timeout-minutes: 30 steps: - uses: actions/checkout@v6

- name: Select Xcode 26.2
  uses: maxim-lobanov/setup-xcode@v1
  with:
    xcode-version: "26.2"

- name: Setup Bun
  uses: oven-sh/setup-bun@v2

- name: Install auto-mobile CLI
  run: bun install -g @kaeawc/auto-mobile

- name: Ensure iOS Simulator runtime
  uses: ./.github/actions/ensure-ios-simulator-runtime

- name: Download build products
  uses: actions/download-artifact@v7
  with:
    name: build-for-testing-macos-26
    path: build/DerivedData/Build/Products/

- name: Boot iOS Simulator
  run: bash scripts/ios/boot-simulator.sh

- name: Start simulator log stream
  run: |
    mkdir -p build
    xcrun simctl spawn booted log stream \
      --level debug --style compact --color none \
      --predicate 'subsystem BEGINSWITH "com.example.ios"' \
      > build/automobile-simulator.log 2>&1 &
    echo "LOG_STREAM_PID=$!" >> "$GITHUB_ENV"

- name: Start AutoMobile daemon
  run: |
    auto-mobile --daemon start &
    echo "AUTOMOBILE_DAEMON_PID=$!" >> "$GITHUB_ENV"

- name: Wait for daemon and register simulator
  run: auto-mobile --cli observe --platform ios

- name: Run AutoMobile tests
  run: bash scripts/ios/xcode-automobile-tests.sh

- name: Stop AutoMobile daemon
  if: always()
  run: kill "$AUTOMOBILE_DAEMON_PID" 2>/dev/null || true

- name: Stop simulator log stream
  if: always()
  run: kill "$LOG_STREAM_PID" 2>/dev/null || true

- name: Upload test results
  uses: actions/upload-artifact@v6
  if: always()
  with:
    name: automobile-test-results
    path: build/automobile-tests.xcresult
    retention-days: 7

- name: Upload simulator logs on failure
  uses: actions/upload-artifact@v6
  if: failure()
  with:
    name: automobile-simulator-logs
    path: build/automobile-simulator.log
    retention-days: 7

```

Step-by-step breakdown

1. Bun and auto-mobile

The daemon is a Node/Bun application. setup-bun ensures the Bun runtime is available on the runner so auto-mobile --daemon start works correctly.

yaml - uses: oven-sh/setup-bun@v2 - run: bun install -g @kaeawc/auto-mobile

2. Build products artifact

AutoMobile tests run test-without-building against the .xctestrun produced by the build-for-testing job. Downloading the artifact avoids recompiling on the test runner:

yaml - uses: actions/download-artifact@v7 with: name: build-for-testing-macos-26 path: build/DerivedData/Build/Products/

The artifact contains both test bundles (YourAppTests.xctest and YourAppAutoMobileTests.xctest) and a .xctestrun file that references them.

3. Boot the simulator

bash bash scripts/ios/boot-simulator.sh

The helper script finds the simulator that matches the current Xcode SDK, creates it if missing, and boots it. Alternatively:

bash xcrun simctl boot "iPhone 16" open -a Simulator xcrun simctl list devices booted # confirm it booted

4. Daemon startup

Start the daemon in the background and save its PID for clean shutdown:

bash auto-mobile --daemon start & echo "AUTOMOBILE_DAEMON_PID=$!" >> "$GITHUB_ENV"

The --cli observe step below implicitly waits for the daemon to be ready, so no explicit socket poll is needed here.

5. CtrlProxy pre-installation

bash auto-mobile --cli observe --platform ios

This step does two things:

  1. Waits for the daemon socket to become available (blocks until the daemon is up).
  2. Installs the CtrlProxy iOS app into the booted simulator if it is not already present, so the first observe step in a test plan does not pay the installation cost.

Running this once before the test task ensures CtrlProxy is ready before observe steps in test plans execute.

6. Run AutoMobile tests

bash bash scripts/ios/xcode-automobile-tests.sh

The script finds the .xctestrun, identifies the booted simulator, and runs:

bash xcodebuild test-without-building \ -xctestrun "$xctestrun_file" \ -destination "platform=iOS Simulator,id=$booted_udid" \ -only-testing:YourAppAutoMobileTests \ -enableCodeCoverage NO \ -skipMacroValidation

-only-testing:YourAppAutoMobileTests limits execution to the AutoMobile bundle. Unit tests in YourAppTests are handled by the separate simulator-tests job.

7. Cleanup

The if: always() guards ensure cleanup runs even when tests fail:

bash kill "$AUTOMOBILE_DAEMON_PID" 2>/dev/null || true kill "$LOG_STREAM_PID" 2>/dev/null || true

The daemon process exits cleanly; the simulator remains booted for the remainder of the runner’s lifetime (macOS runners are ephemeral so no explicit teardown is needed).

8. Artifacts

Artifact When uploaded Contents
automobile-test-results Always build/automobile-tests.xcresult — pass/fail, timing, screenshots
automobile-simulator-logs On failure build/automobile-simulator.log — simulator log stream filtered to your app’s subsystem

Keeping unit tests and AutoMobile tests separate

The simulator-tests job runs unit tests using -skip-testing:YourAppAutoMobileTests:

```bash

scripts/ios/xcode-test-without-building.sh

-skip-testing:YourAppAutoMobileTests excludes the AutoMobile bundle

xcodebuild test-without-building \ -xctestrun “$xctestrun_file” \ -destination “platform=iOS Simulator,id=$booted_udid” \ -skip-testing:YourAppAutoMobileTests \ -enableCodeCoverage NO \ -parallel-testing-enabled YES \ -skipMacroValidation ```

The automobile-tests job runs the complement using -only-testing:YourAppAutoMobileTests. This mirrors the Android pattern where testDebugUnitTest -PexcludeAutoMobileTests runs unit tests and testDebugUnitTest --tests '*.automobiletest.*' runs the AutoMobile tests.

Caching strategy

The build-for-testing job uses two cache layers that the automobile-tests job benefits from indirectly through the artifact:

Cache Key What it stores
SPM packages runner-xcode<ver>-spm-<hash(project.yml)> Alamofire, XCTestRunner, etc. — avoids re-cloning
DerivedData intermediates runner-xcode<ver>-intermediates-<hash(sources+configs)> Compiled .o and .swiftmodule files — makes incremental builds fast

Both caches are keyed so a clean rebuild only triggers when sources actually change.

Required secrets

No additional secrets are required for a basic AutoMobile test run. The macOS runner has Xcode and iOS Simulator built in, and the daemon communicates entirely over a local Unix socket.

If you add features that require external services (e.g., a staging backend with an API key), pass them as environment variables through the test scheme:

yaml - name: Run AutoMobile tests env: AUTOMOBILE_TEST_API_KEY: ${{ secrets.TEST_API_KEY }} run: bash scripts/ios/xcode-automobile-tests.sh

Resolve them in the plan via parameter substitution (see Writing Tests → Plan parameters).

Conditional execution

To skip the job on draft pull requests or when a specific label is absent:

yaml automobile-tests: if: github.event.pull_request.draft != true

Reading results from CI

Download the automobile-test-results artifact and inspect with xcresulttool:

```bash xcrun xcresulttool get test-results summary \ –path automobile-tests.xcresult

xcrun xcresulttool get test-results tests \ –path automobile-tests.xcresult –format json \ | jq ‘[.. | objects | select(.testStatus? == “Failure”) | .nodeIdentifier]’ ```

See also