
Register a Figment M-series Mac from $95/month (specific chip, RAM, and disk confirmed in the quote; physical plans collect requests until capacity is confirmed) as a GitLab shell executor runner once your macOS CI exceeds the break-even hours against GitLab SaaS compute minutes, worked below. For lower-cadence iOS jobs pinned to an Xcode version that still supports Intel, the virtual Intel macOS VM at $29/month (2 vCPU, 8 GiB RAM, 80 GiB disk; no GPU or audio) or i3 Mac at $49/month can host a shell runner. Teams who want the runner, backups, and monitoring installed and maintained should pick the Managed Build Mac at $249/month (one M-series Mac, CI setup, backups, monitoring, 30 minutes of monthly support) or Managed Mac Team at $999/month (two M-series Macs, two CI runners, team setup, backups, monitoring, one hour of monthly support).
GitLab SaaS provides hosted macOS runners, but jobs consume compute minutes at a multiplied rate, and the hardware class is fixed to what GitLab offers. Registering a Figment Mac as a self-hosted shell executor gives your team a persistent macOS environment with no per-minute meter. This guide covers the install, registration, Xcode selection, a working .gitlab-ci.yml, and the security pattern that matters when a single Mac serves a team.
GitLab's hosted macOS runners multiply job duration by a cost factor before debiting your quota. GitLab's compute minutes documentation lists macOS M1 medium at 6× and macOS M2 Pro large at 12× (both marked Beta at time of writing — check the page before you commit). Additional compute minutes on GitLab's pricing page are sold at $10 per 1,000 minutes, i.e. $0.01 per compute minute.
Combining both: one minute of real time on an M1 medium runner costs 6 × $0.01 = $0.06 once your included quota is exhausted. A Figment M-series Mac from $95/month breaks even at:
Above that, a self-hosted runner is cheaper than buying additional minutes, and it also removes the quota as a release-day bottleneck. This is an entry-price comparison, not a same-hardware claim: Figment's $95 entry tier is an M-series Mac whose exact chip is set in the quote, and GitLab's M1 medium is a specific M1 class.
Install the runner from Homebrew on an M-series Mac, or from the ARM64 binary. Both paths are documented in GitLab's macOS installer guide:
brew install gitlab-runner
gitlab-runner --version
Or install the binary directly:
curl -Lo /usr/local/bin/gitlab-runner \
https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-darwin-arm64
chmod +x /usr/local/bin/gitlab-runner
GitLab's docs require that you perform this step while signed in locally to the Mac (local terminal or screen-sharing session), not over SSH. The shell executor runs as a user-mode LaunchAgent and needs an active user session to reach the Keychain and Simulator. On a Figment M-series Mac, use the browser console or your remote desktop session to open Terminal; on the virtual Intel macOS VM, the desktop terminal is the verified access path (inbound SSH is not currently implemented).
Create a project runner token in GitLab: Settings → CI/CD → Runners → New project runner. Choose a tag such as mac-arm64 (or mac-intel on the $29/$49 tiers) and copy the token.
gitlab-runner register
Answer the prompts:
https://gitlab.com or your self-managed URLfigment-mac-arm64mac-arm64shellThe resulting ~/.gitlab-runner/config.toml should look like:
[[runners]]
name = "figment-mac-arm64"
url = "https://gitlab.com"
token = "glrt-xxxxxxxxxxxx"
executor = "shell"
[runners.cache]
Install the LaunchAgent and start it:
gitlab-runner install
gitlab-runner start
Enable automatic login so the agent is available after reboot; otherwise the runner stays offline until a human signs in.
When several Xcode versions are installed side by side, set DEVELOPER_DIR as a job variable instead of switching system-wide with xcode-select. This lets parallel jobs target different Xcode versions on the same runner without racing each other:
variables:
DEVELOPER_DIR: /Applications/Xcode_26.0.app/Contents/Developer
Confirm the path on the Mac before you commit it:
ls /Applications/ | grep -i Xcode
/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" \
/Applications/Xcode_26.0.app/Contents/Info.plist
Intel Macs are limited to Xcode 26 or earlier per Apple's porting guide; check the exact Xcode and macOS pair you need against Apple's release notes before you pin it in CI.
Replace MyApp, the workspace path, and the scheme with your real project's values before committing. The example is illustrative; run it against your repository and fix whatever your build actually reports.
stages: [build, test, upload]
variables:
DEVELOPER_DIR: /Applications/Xcode_26.0.app/Contents/Developer
DERIVED_DATA: ${CI_PROJECT_DIR}/.derived-data
default:
tags: [mac-arm64]
cache:
key:
files: [Podfile.lock]
paths:
- Pods/
- vendor/bundle/
build_ios:
stage: build
script:
- set -euo pipefail
- pod install --deployment
- xcodebuild archive
-workspace "$APP_WORKSPACE"
-scheme "$APP_SCHEME"
-configuration Release
-archivePath build/MyApp.xcarchive
-derivedDataPath "$DERIVED_DATA"
artifacts:
paths: [build/MyApp.xcarchive]
expire_in: 1 day
test_ios:
stage: test
script:
- set -euo pipefail
- mkdir -p test-results
- xcodebuild test
-workspace "$APP_WORKSPACE"
-scheme "$APP_SCHEME"
-destination "platform=iOS Simulator,name=iPhone 16,OS=latest"
-derivedDataPath "$DERIVED_DATA"
-resultBundlePath test-results/TestResults.xcresult
artifacts:
when: always
paths: [test-results/]
expire_in: 1 week
upload_testflight:
stage: upload
rules:
- if: $CI_COMMIT_BRANCH == "main"
script:
- set -euo pipefail
- bash ci/export-ipa.sh
- xcrun altool --upload-app
--type ios
--file "$(find build/export -name '*.ipa' | head -1)"
--apiKey "$ASC_API_KEY_ID"
--apiIssuer "$ASC_API_ISSUER_ID"
dependencies: [build_ios]
APP_WORKSPACE and APP_SCHEME live in GitLab CI/CD variables, not in the YAML, so a forked project uses its own scheme. set -euo pipefail makes every stage fail fast — a failed build must never be timed as a successful benchmark.
The shell executor runs as the macOS user who registered the runner, with full access to that user's Keychain and home directory. Treat the runner account the same as any other production credential.
| Risk | Mitigation |
|---|---|
| CI job reading a developer's signing certificate | Register the runner under a dedicated ci account; import the CI signing identity only there, as an encrypted .p12 with its private key, and place the provisioning profile file in ~/Library/MobileDevice/Provisioning Profiles/ |
| Secrets leaked through job logs | Store API keys as GitLab CI/CD variables marked Masked and Protected; never env > .env to a plaintext file — it will include credentials. Reference each required secret by name and let your secret manager populate it |
| Jobs reading each other's workspace | Set builds_dir under [[runners]] in config.toml to a project-scoped path, or dedicate a runner per project |
| Allowlists on third-party services | Update your own artifact store, package registry, or internal services to accept the Mac's egress IP. There is no published App Store Connect or TestFlight inbound IP range to allow — uploads originate from the Mac |
Confirm the runner is online and picked up by its tag in Settings → CI/CD → Runners before you point a release branch at it.
For most iOS teams the useful choice is the M-series Mac from $95/month as the shell executor, when the workflow needs Xcode 27, ARM-only runtimes, or Apple silicon performance. Above roughly 26 hours of macOS CI per month, that one Mac is cheaper than buying additional GitLab compute minutes at the M1-medium multiplier, and the runner is always on. The virtual Intel macOS VM at $29/month and i3 Mac at $49/month are rational hosts if your pipeline is pinned to Xcode 26 or earlier and you do not need GPU, audio, or Apple-silicon-only simulators; both collect requests until supply is confirmed. For a managed path that includes the CI setup, backups, monitoring, and monthly support hours listed above, choose Managed Build Mac $249/month or Managed Mac Team $999/month.
Start your Mac runner at /mac-hosting/start, or see /mac-hosting for the full plan list.
Related guides