Figment.so
Mac hostingWebsite hostingGuidesHow to usePricing

Build and Test Swift Packages in CI on Mac

For Swift packages that use macros, C/C++ interop, or Apple-silicon-only dependencies, run this on a Figment M-series Mac from $95/month (exact chip, RAM, and disk confirmed at quote; physical allocation is request-based until supply is confirmed). For pure Swift libraries that build on both architectures and target a Swift toolchain shipped with Xcode 26 or earlier, the virtual Intel macOS VM at $29/month (2 vCPU, 8 GiB RAM, 80 GiB disk) runs swift build and swift test as a lower-cost entry point; it is virtual Intel macOS on Linux/KVM with no GPU acceleration or audio, and access is through the browser console with outbound CI agents connecting to your service. Teams who want the runner configured for them can take Managed Build Mac at $249/month, which includes one M-series Mac, CI setup, backups, monitoring, and 30 minutes of monthly support.

If the project is a pure Swift package — no app bundle, no storyboards — swift build and swift test give a tighter feedback loop than xcodebuild. They read Package.swift directly and can emit machine-readable test output, as documented by the Swift Package Manager documentation. Running the same tests on Linux in the same workflow catches platform-specific bugs early. Here is a working pipeline for a Figment Mac runner.

swift build vs xcodebuild in CI

Use swift build when:

  • The project has no .xcodeproj and no Xcode-specific build settings
  • You want parity with Linux (xcodebuild is macOS-only)
  • You want a faster cold start without the xcbuild layer

Use xcodebuild when:

  • There is an app target, extension, or asset catalog
  • You need code signing, entitlements, or .ipa export
  • You are building against an Xcode-managed SDK or UI test plan

A single repository can use both: swift build for the library in CI and xcodebuild for a host app or UI tests.

Pin the Swift toolchain version

Each Xcode version ships one Swift toolchain. On a hosted Mac, xcode-select chooses which installed Xcode swift resolves to:

# List installed Xcode versions
ls /Applications | grep -i '^Xcode'

# Point the toolchain at a specific installation
sudo xcode-select -s /Applications/Xcode_16.3.app

# Confirm before anything else runs
xcodebuild -version
swift --version

Make the selection a named step in the workflow so the active version appears in the log and is trivial to bump. The exact Swift version shipped with each Xcode release is listed in Apple's Xcode release notes; verify the Xcode/macOS pair against Apple's Xcode release notes before pinning. On the $29 Intel VM, keep to Xcode 26 or earlier (per Apple's porting guidance at developer.apple.com); for ARM-only toolchains or Swift macros, use an M-series host.

swift test with --parallel and XUnit output

--parallel distributes test cases across cores. Historically, SwiftPM only wrote --xunit-output when --parallel was set for XCTest suites (swiftlang/swift-package-manager#7065); keep --parallel in the command to stay on the supported path. When the package also contains Swift Testing suites, SwiftPM writes a second XML file alongside the XCTest one, with the -swift-testing.xml suffix.

mkdir -p test-results

swift test \
  --parallel \
  --xunit-output test-results/results.xml

After the run, expect:

  • test-results/results.xml for XCTest cases
  • test-results/results-swift-testing.xml if Swift Testing suites exist

Upload the directory so the summary step picks up both files.

Cache the .build directory

SwiftPM keeps its incremental build products in .build. Restoring that directory between runs avoids recompiling dependencies that did not change. A good cache key hashes Package.resolved (which pins every resolved dependency), and partitions by configuration and runner OS:

- uses: actions/cache@v4
  with:
    path: .build
    key: "${{ runner.os }}-spm-${{ matrix.configuration }}-${{ hashFiles('Package.resolved') }}"
    restore-keys: |
      ${{ runner.os }}-spm-${{ matrix.configuration }}-

When a dependency bump changes Package.resolved, the primary key misses and the restore key falls back to the newest partial match, so unchanged modules still skip recompilation.

Build release to catch optimizer-only errors

Some bugs only surface with -O. Add a release build as a second matrix leg:

swift build -c release

This only compiles; it does not run tests. If the package relies on @inlinable or specialization, release-mode tests (swift test -c release) may be worth the extra minutes, but debug tests plus a release build cover most library projects.

Full GitHub Actions workflow

name: Swift Package CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  build-and-test:
    runs-on: [self-hosted, macOS, figment]
    strategy:
      fail-fast: false
      matrix:
        configuration: [debug, release]

    steps:
      - uses: actions/checkout@v4

      - name: Select Xcode
        run: |
          sudo xcode-select -s /Applications/Xcode.app
          swift --version

      - name: Cache .build
        uses: actions/cache@v4
        with:
          path: .build
          key: "${{ runner.os }}-spm-${{ matrix.configuration }}-${{ hashFiles('Package.resolved') }}"
          restore-keys: |
            ${{ runner.os }}-spm-${{ matrix.configuration }}-

      - name: Build (${{ matrix.configuration }})
        run: swift build -c ${{ matrix.configuration }}

      - name: Test (debug only)
        if: matrix.configuration == 'debug'
        run: |
          mkdir -p test-results
          swift test \
            --parallel \
            --xunit-output test-results/results.xml

      - name: Upload test results
        if: always() && matrix.configuration == 'debug'
        uses: actions/upload-artifact@v4
        with:
          name: swift-test-results
          path: test-results/
          retention-days: 30

actions/upload-artifact@v4 returns a SHA-256 digest output that the download action re-verifies, per the GitHub Actions artifact documentation; treat it as the integrity check rather than rolling your own checksum step. retention-days is capped by the repository, organization, or enterprise retention limit — the default is inherited from that setting, not hard-coded here. This example is illustrative, not a benchmark: it has not been timed, and the failing steps use set -e semantics so a failed build cannot be reported as a fast run.

macOS and Linux in the same pipeline

Swift packages often target both platforms. Add a Linux job in parallel:

  test-linux:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Test on Linux
        run: |
          mkdir -p test-results
          swift test \
            --parallel \
            --xunit-output test-results/linux-results.xml
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: swift-test-results-linux
          path: test-results/

If a test passes on macOS but fails on Linux (or vice versa), the matrix exposes it on the pull request without a second workflow file.

Picking the Figment plan for this job

A worked worksheet, using only approved prices:

ScenarioFigment planMonthlyNotes
Pure Swift library, Xcode 26 or earlier, no ARM-only depsVirtual Intel macOS VM$292 vCPU, 8 GiB RAM, 80 GiB; browser console; outbound CI agent
Library uses Swift macros, C/C++ interop, or ARM-only depsM-series Macfrom $95Specific chip/RAM/disk confirmed at quote; request-based allocation
Want the runner, agent, and backups configured for youManaged Build Mac$249One M-series Mac, CI setup, backups, monitoring, 30 min monthly support
Two-runner team with team setupManaged Mac Team$999Two M-series Macs, two CI runners, backups, monitoring, 1 hour monthly support

Swift macros execute as native host plugins during the build, so a macro-using package on the Intel VM will either refuse to build or build only the non-macro subset — that is the practical line between the $29 and the $95 tiers. If the package compiles cleanly on Intel today and tests pass, the VM is a legitimate option; if it does not, move to M-series.

Start a runner at /mac-hosting/start, or read the plan overview at /mac-hosting.

Related guides


Subscribe for Figma design and website publishing guides.


Figment.so

Contact

Twitter

Website pricing

Compute index

About

Privacy

Terms