Debug a failing iOS CI build on a cloud Mac
Stop pushing commits to guess at a CI failure. Reproduce it on a clean macOS VM, let your agent bisect the fix, then send one commit back to CI.
· 8 min read
Need a Mac for this? NoMac gives your agent full macOS with Xcode, from $9.99 a month, billed by the second. Start now →
Every iOS developer knows this loop. The build passes on your machine and fails in CI. You read the log, guess, push a commit called “fix ci”, wait twelve minutes, and read a slightly different log. Five commits later you have fixed it, or given up and added a retry.
If you do not own a Mac, it is worse: you cannot even reproduce the passing build locally. The faster path is to reproduce the CI environment on a clean Mac you can talk to, fix it there in minutes, and send one commit back.
Why a fresh VM is the right place: CI runners start clean. A clean macOS VM reproduces the failures your laptop hides: files that are not committed, schemes that are not shared, caches only you have, signing settings tied to one keychain.
1. Start from exactly what CI built
git clone --branch failing-branch https://github.com/you/MyApp.git cd MyApp nomac start --json nomac sync .
Clone fresh rather than syncing your working copy. If the failure is “works on my machine”, your working copy is the thing hiding it.
2. Match the toolchain
nomac ssh -- xcodebuild -version nomac ssh -- xcrun simctl list runtimes
Compare with the Xcode version your CI job selects. A surprising number of CI failures are simply a different Xcode: a new warning promoted to an error, a changed Swift concurrency check, or a deployment target the older SDK does not know. If versions differ, note it: that alone may be the answer.
3. Run the exact CI command
Copy the build command from the CI log, not from memory. Flags matter: -destination, -configuration, -derivedDataPath, CODE_SIGNING_ALLOWED=NO.
nomac ssh -- xcodebuild -workspace MyApp.xcworkspace -scheme MyApp \ -configuration Release -destination 'generic/platform=iOS' \ CODE_SIGNING_ALLOWED=NO build 2>&1 | tail -80
If it fails the same way, you have a reproduction and can iterate in seconds rather than pushes.
4. The usual suspects
Swift Package resolution
Errors like Could not resolve package dependencies usually mean Package.resolved is missing or not committed, a private package needs credentials CI does not have, or a branch dependency moved. Run xcodebuild -resolvePackageDependencies on its own to isolate it.
CocoaPods
A missing Pods directory, a Podfile.lock mismatch, or a different CocoaPods or Ruby version. Run bundle exec pod install --deployment to see the same error CI sees.
A scheme that is not shared
xcodebuild: error: The project does not contain a scheme named ... means the scheme lives in your user data, not in xcshareddata. Mark it shared and commit it.
Files that only exist on your machine
A missing GoogleService-Info.plist, a generated file, or a config that is in .gitignore. The fresh clone reproduces this immediately.
Signing in a job that should not sign
Errors about provisioning profiles in a test or analysis job. Build with CODE_SIGNING_ALLOWED=NO for anything that does not ship. See code signing errors, decoded for the rest.
Out of memory or time
A build that dies without an error, or is killed at a round number of minutes, is usually the runner, not your code. Check the CI job's timeout and machine size before touching source.
5. Let your agent bisect it
This is a good job for an agent: it is repetitive, the success condition is clear, and it can try things you would not bother to. Give it the reproduction and the rule:
The CI build fails; the reproduction is on the NoMac session. Change one thing at a time, sync, rerun the exact CI command, and record the result. Stop when it passes. Then summarise the minimal fix as one commit. Do not change CI configuration unless the source cannot be fixed.
6. Send one commit back
Once it passes on the clean Mac, commit the fix, push once, and let CI confirm. Then stop the session:
nomac stop --json
- ·A dozen failed CI runs cost more minutes than one short interactive session.
- ·Your Git history keeps one meaningful commit instead of six guesses.
- ·You learn why it failed, which is what stops it happening again.
CI stays the source of truth; the session is a workbench. For the difference in shape, see NoMac vs GitHub Actions macOS runners.