
GitHub-hosted macOS runners bill at $0.062 per minute on the standard 3- or 4-core macOS image (About billing for GitHub Actions). A 20-minute iOS build run 100 times a month is $124; 200 runs is $248. Queues on the shared macOS pool add wall-clock time on top of that bill, and every run starts from a cold toolchain without your Pods or DerivedData.
Self-hosted runners are free to use with Actions; you pay the fixed cost of the host instead (About self-hosted runners). A Figment M-series Mac starts at $95/month — the entry price for an M-series configuration; the exact chip, RAM, and disk are set in your quote, not advertised as a fixed spec. Against $0.062/minute, $95 covers 1,532 build-minutes per month (77 twenty-minute builds, or 153 ten-minute builds). Past that, the flat rate wins.
For teams that would rather not run the runner themselves, Figment's Managed Build Mac at $249/month adds one M-series Mac, CI setup, backups, monitoring, and 30 minutes of monthly support. The Managed Mac Team at $999/month covers two M-series Macs, two CI runners, team setup, backups, monitoring, and one hour of monthly support.
One hardware caveat before you choose a plan: the $29 virtual Intel macOS VM (2 vCPU, 8 GiB RAM, 80 GiB disk) can run compatible Intel Mac toolchains, including supported Xcode 26 or earlier on Intel (Porting your macOS apps to Apple silicon). It has no GPU acceleration, no audio, and cannot execute ARM-only binaries or visionOS simulators. For ARM builds, SwiftUI previews that need a real GPU, or visionOS, pick an M-series plan.
| Requirement | Detail |
|---|---|
| Figment M-series Mac | From $95/month — request a configuration quote |
| GitHub permissions | Admin on the target repository (or org for org-level runners) |
| Runner registration token | Settings → Actions → Runners → New self-hosted runner (tokens expire within the hour) |
| Shell access to the Mac | Access method is set in your Figment quote; the commands below assume a terminal session on the host |
| Xcode | Installed at a known versioned path, e.g. /Applications/Xcode_16.2.app |
| Homebrew + CocoaPods | Pre-installed so CI jobs do not trigger interactive installs |
Copy the exact download URL and token from the "New self-hosted runner" screen — the versioned URL changes with each runner release, so don't hard-code it.
Open a terminal on your Figment Mac and run these commands, substituting the values GitHub shows you:
mkdir -p ~/actions-runner && cd ~/actions-runner
# GitHub's dialog shows the exact versioned URL — paste it here
curl -o actions-runner-osx-arm64.tar.gz -L \
"https://github.com/actions/runner/releases/download/vX.Y.Z/actions-runner-osx-arm64-X.Y.Z.tar.gz"
tar xzf ./actions-runner-osx-arm64.tar.gz
# Configure with your org/repo URL and the token from the dialog
./config.sh \
--url "https://github.com/YOUR_ORG/YOUR_REPO" \
--token "YOUR_REGISTRATION_TOKEN" \
--name "figment-mac-01" \
--labels "self-hosted,macOS,figment-m-series,ios-ci" \
--work "_work" \
--unattended
# Install as a persistent launchd service and start it
./svc.sh install
./svc.sh start
The runner should appear in Settings → Actions → Runners with an Idle status shortly after. The --labels flag is what lets workflows target this exact machine. Keep self-hosted and macOS as the standard pair, then add project-specific labels (figment-m-series, ios-ci) so you can route job types to specific hosts later.
ios.yml workflowSave this to .github/workflows/ios.yml and replace the capitalized placeholders with your own project's workspace and scheme. It targets the labeled runner, pins Xcode, caches Pods and DerivedData, and exits non-zero on build failure — this file is an illustrative example, not a benchmarked run.
name: iOS CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build-and-test:
# Only lands on your registered Figment Mac, never GitHub-hosted
runs-on: [self-hosted, macOS, figment-m-series]
env:
XCODE_VERSION: "16.2" # Pin — bump deliberately, not silently
DERIVED_DATA: ~/Library/Developer/Xcode/DerivedData/CI
steps:
- uses: actions/checkout@v4
- name: Select Xcode ${{ env.XCODE_VERSION }}
run: sudo xcode-select -s /Applications/Xcode_${{ env.XCODE_VERSION }}.app
- name: Restore CocoaPods cache
id: pods-cache
uses: actions/cache@v4
with:
path: |
Pods
~/.cocoapods/repos
key: pods-${{ runner.os }}-${{ hashFiles('**/Podfile.lock') }}
restore-keys: |
pods-${{ runner.os }}-
- name: Install pods
if: steps.pods-cache.outputs.cache-hit != 'true'
run: pod install --repo-update
- name: Restore DerivedData cache
uses: actions/cache@v4
with:
path: ${{ env.DERIVED_DATA }}
key: derived-${{ runner.os }}-${{ hashFiles('**/*.xcodeproj/project.pbxproj') }}
restore-keys: |
derived-${{ runner.os }}-
- name: Build and test
run: |
set -euo pipefail
xcodebuild test \
-workspace YOUR_WORKSPACE.xcworkspace \
-scheme YOUR_SCHEME \
-destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \
-derivedDataPath "$DERIVED_DATA" \
-resultBundlePath TestResults.xcresult \
| xcpretty
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-${{ github.run_id }}
path: TestResults.xcresult
set -euo pipefail on the build step matters: when piping to xcpretty, bash runs both processes concurrently, and without pipefail the step exits zero even when xcodebuild fails. A failed build must never be counted as a successful fast run.
On a persistent Figment Mac the working directory survives between jobs, so DerivedData that didn't make it into the Actions cache still lives on disk until something clears it. Use the cache action for portability and to seed fresh workspaces, but don't put credentials in it.
| Path | Cache? | Why |
|---|---|---|
Pods/ | Yes | Skips full re-install when Podfile.lock hasn't changed |
~/.cocoapods/repos | Yes | Avoids re-cloning the specs repo |
| DerivedData | Yes | Reuses compiled intermediates |
Signing certificates / .p12 | No | Install from an encrypted CI secret into a temporary keychain each run, then delete |
| Provisioning profiles | No | Deliver as files through the CI secret store; they are not exported from Keychain |
.env files | No | Never export the shell environment to a plaintext file — it can contain keys. Transfer required secret names and restore values from your CI secret manager |
Update allowlists only on infrastructure you actually control — your own artifact store, package proxy, or CI service. Don't assume fixed IP ranges for third-party developer services.
GitHub's guidance treats self-hosted runners as unsuitable for public repositories because anyone who can open a pull request can run code on your host (Security hardening for self-hosted runners). The practical controls:
concurrency: group: ${{ github.ref }} cancels in-flight runs on new pushes, shortening the exposure window on long jobs.GitHub-hosted macOS bills by the minute with no monthly cap; a Figment M-series runner is flat. At $95/month vs $0.062/minute the crossover is 1,532 build-minutes per month.
| Monthly build volume | GitHub-hosted macOS | Figment M-series (from $95/mo) |
|---|---|---|
| 500 min (25 × 20 min) | $31.00 | $95 |
| 1,000 min (50 × 20 min) | $62.00 | $95 |
| ~1,532 min — break-even | ~$95 | $95 |
| 2,500 min (125 × 20 min) | $155.00 | $95 |
| 5,000 min (250 × 20 min) | $310.00 | $95 |
This compares monthly cost at a given build volume, not identical hardware. Below ~1,532 build-minutes you save money staying hosted and avoid any runner maintenance. Above it, a Figment Mac is cheaper and your queue is yours. If you'd rather hand off the runner itself, Managed Build Mac ($249) or Managed Mac Team ($999) covers setup, backups, monitoring, and a defined block of support time each month.
Start a Figment M-series Mac configured for CI →
Related guides: