← Back to journal

Fastlane Dual Cloud Mac Pitfalls: Top 3 Failure Modes After Splitting Build and Upload

Runbook · 2026.06.04 · ~8 min read · paste into on-call doc

Dual cloud Macs: Hong Kong node archives, Canada node Fastlane upload

If you are on call with two or more rented cloud Macs running Fastlane for iOS releases, and you recently saw green compile logs but no new TestFlight build (or the wrong package uploaded), this article is for you. Still deciding whether to move to the cloud or add a second machine? Read the TestFlight dedicated runner or moving Xcode builds to the cloud first — here we assume you already split one machine to build, one to upload.

After reading, you should leave with two things: ① which class of problem the logs point to — certificates, artifacts, or network; and ② whether your two machines have the right roles, plus whether buying more hardware would actually help.

Terms we use below
  • Build machine: compiles the project in Xcode and exports an installable package (.ipa)
  • Upload machine: signs with the release certificate and uploads the .ipa to TestFlight; the egress IP registered in App Store Connect should match this host only
  • Archive / “the build”: Xcode’s distributable output; between machines we hand off only the finished .ipa, not the whole project cache folder
  • match: Fastlane tool that manages release certificates and provisioning profiles — run on one machine only

You can skip this if: you only have one Mac, or TestFlight uploads have been fine and you have not split into separate build and upload hosts. If you already use multiple cloud Macs for release and the pipeline is failing, keep reading.

TL;DR (30 seconds)
After a dual-cloud Mac split, the bottleneck is rarely CPU. It's usually three Fastlane boundary failures:
  1. Certificate boundary — the release certificate was synced to the build machine, so the upload machine cannot sign (most dangerous)
  2. Artifact boundary — moving .xcarchive between datacenters instead of a verifiable .ipa (easiest to miss)
  3. Network identity drift — App Store Connect still whitelists an old upload IPv4 (hardest to debug)
Below: a 30-second triage tree, then the three failure modes in detail; at the end, an acceptance checklist, four CI stages, and a final verdict block you can act on without rereading the whole post.

Context (1 minute): With two Macs, boundaries break more often than CPUs

Many teams rent two cloud Macs: one closer to Asia to compile the app into an install package during the day, and one in Canada to deliver the package to Apple (TestFlight / App Store). The second machine is often about speed — but on-call time usually goes to both machines touching certificates and files: the build host shows success while TestFlight never gets a new version.

Below we first describe the common wrong split (Model B), then the split we use (Model A) and how to verify it.

Recommended split: Model A (build only packages, upload only publishes)

In one line: the build machine only produces an .ipa; the upload machine owns certificates and TestFlight. Do not run certificate sync (match) on both hosts — the private key lands on the wrong machine and the upload host reports a missing distribution certificate.

What each cloud Mac does (Model A)
Role Build machine (Hong Kong) Example: daytime builds in APAC Upload machine (Canada) Fixed IP for Apple
DoPull code → Xcode build → export .ipaReceive .ipa → release signing → upload to TestFlight
Do notNo match, no TestFlight uploadDo not run a full project compile
Hand off to the other hostInstall package file + build ID (RUN_ID)None (endpoint is App Store Connect)

For engineers mapping to Fastlane: the build host’s lanes only compile and export; the upload host has match, upload_to_testflight, and the App Store Connect API key. Register only the upload host’s egress IP in Apple’s backend.

Build machine · packages only Upload machine · delivers to Apple build → export .ipa no certs · no upload includes build ID certs + TestFlight upload fixed public IP registered in ASC .ipa → Hand off only the install package between hosts — not the full project cache
Model A: build delivers .ipa only; upload handles certificates and TestFlight

Both runners sit on dedicated Hashvps Mac minis — mainly for fixed IP + role isolation (upload IP in ASC, build IP not). Hong Kong specs: Hong Kong Mac mini plans. Lock upload identity in ASC before you horizontally scale build runners.

Common mistake: Model B (both machines “helping”)

Model B means roles were never separated — we ran it this way in week one. It looks like two machines sharing work, but in practice:

  • Both hosts run match → the release private key stays on the build machine → the upload machine cannot sign → TestFlight keeps failing
  • Copying the whole .xcarchive folder to the other host to export again → mismatched configs → Apple rejects the package
  • Each host exports its own .ipa → different export options → logs say success, TestFlight has no usable build

If any line matches your pipeline, a third Mac usually will not help — clarify who builds, who uploads, and who owns certificates using Model A first.

Observability: prove build and upload are the same run

The most common ops mistake is uploading last night’s IPA. We print the same fields in build and upload logs:

  • RUN_ID (pipeline run, e.g. hk-build-88421)
  • GIT_SHA (short commit hash)
  • Artifact path + SHA256 (written to build/manifest.json)
Build succeeded · upload succeeded (same RUN_ID)
# Hong Kong · build
[15:11:42]: Successfully exported: ./build/MyApp-20260604-1502.ipa
[15:11:42]: RUN_ID=hk-build-88421  GIT_SHA=a3f91c2  SHA256=9f2a…

# Canada · upload
[02:22:01]: RUN_ID=hk-build-88421  GIT_SHA=a3f91c2  ipa_sha256=9f2a…
[02:24:18]: Successfully uploaded package to App Store Connect

On-call rule: if the upload log doesn’t show the same RUN_ID, treat it as an artifact mismatch first — not ASC or Apple status pages.

30-second triage: no new TestFlight build?

Don’t scroll the full log yet. One level in the tree is enough to land on the right section (bookmark this for your runbook):

On-call triage tree (copy into runbook)
No new TestFlight build?
 ├─ Upload RUN_ID ≠ build RUN_ID (or missing manifest)
 │     → Pitfall 2 · artifact boundary
 ├─ Logs mention match / AppStoreDistribution / code signing identity
 │     → Pitfall 1 · certificate boundary
 └─ Upload “success” but ASC empty / 403 / Invalid Binary (build all green)
       → Pitfall 3 · network identity drift
Three failure modes · quick reference (on-call)
Pattern Log keywords (grep) Release impact
Pitfall 1 · certificate AppStoreDistribution, No matching provisioning, match failed Blocker: TestFlight / store upload stopped
Pitfall 2 · artifact RUN_ID mismatch, Invalid Binary, export_method conflict High: wrong build or can’t promote
Pitfall 3 · identity 403, upload OK with no processing, ASC network limits Intermittent: feels random

Three failure modes (expanded)

Pitfall 1: certificate boundary wrong

30 seconds: build archive green; upload red, failing at match / signing.

Log: AppStoreDistribution, Could not find a matching code signing identity, No matching provisioning profiles.

Release: Blocker — TestFlight and App Store uploads stay down until the private key lives only on the upload host.

Symptom: archive succeeds on the build machine; upload lane fails; TestFlight shows no new build.

Typical log
Could not find a matching code signing identity for type 'AppStoreDistribution'
❌  Lane upload failed

Root cause: Model B — the build machine still runs match(appstore); the distribution key is not on the upload machine.

Fix: remove all match from the build machine; upload owns it exclusively (rotate only there, in its own window). See match readonly.

Pitfall 2: artifact boundary wrong

30 seconds: compare RUN_ID and ipa_sha256 in build vs. upload logs; or ASC reports Invalid Binary with no signing error on build.

Log: RUN_ID mismatch, Invalid Binary, double export_ipa / ExportOptions on two hosts.

Release: High — old commit, wrong build, or stuck processing; pipeline may still look green.

Root cause: moving .xcarchive between datacenters or exporting twice. Smallest cross-DC unit: IPA with SHA, not an archive folder.

Fix: one export_ipa + manifest.json on build; upload only uploads, no second export.

Pitfall 3: network identity drift

30 seconds: Fastlane reports upload success; signing green on both hosts; TestFlight empty or ASC 403.

Log: 403, Successfully uploaded with no new build, ASC network rejection despite green build logs.

Release: Intermittent — works sometimes, doesn’t others; eats on-call time.

Root cause: upload IPv4 ≠ entry in the ASC whitelist (third, quiet failure class).

Fix: daily curl -4 ifconfig.me on upload vs. ASC; on drift, update whitelist + alert. Build IPv4 should not be in ASC — a wrong entry there masks real upload drift.

How to know the split actually worked

One checklist instead of scattered FAQ — one week after rollout, every row should be an unambiguous yes:

Dual-Mac acceptance (Model A)
Check Expected
RUN_ID alignedBuild export and upload logs match on RUN_ID, GIT_SHA, ipa_sha256
Build has no DistributionBuild host keychain/logs never show AppStoreDistribution or a match step
Upload is upload-onlyUpload lane: only match (or resign) + upload_to_testflight, no ARCHIVE SUCCEEDED
Artifact shapeCross-DC handoff is .ipa + manifest (< 200 MB); no .xcarchive rsync
ASC identityCurrent upload IPv4 = ASC whitelist; build IP not in ASC
Runner isolationLabels role=ios-build-hk and role=ios-upload-ca, workflow needs chain

Where are you on the maturity curve?

Incident fixes answer “tonight.” Stage answers “change architecture next week?” Four stages — hold, correct, or force role split.

Stage 1: one Mac, one Fastlane

Signals: one runner; match, build, upload in one Fastfile and keychain. Tune disk, Derived Data, and GitHub Actions billing before adding a second build node.

Verdict: no dual-Mac needed. APAC builds slow only → add a build node, don’t split upload first.

Stage 2: two Macs, no role split (Model B)

Signals: two or more runners; multiple run match / export / upload; logs show the three boundary failures in this article.

Verdict:A third Mac fixes nothing; it raises cert and artifact drift. Next step: Model A, not more runners.

Stage 3: role split (Model A · target of this post)

Signals: build/upload separated; cross-DC handoff is IPA with RUN_ID; match only on upload; fixed upload IP.

Verdict: ✔ acceptance table stays green → scale build runners horizontally or harden the upload host. Fixed IPs and per-node ordering: Hashvps plans (lock ASC IP for upload first, then expand build).

Stage 4: multiple upload regions (advanced)

Signals: upload in US/EU/APAC; ASC keys and IP policy per region; promotion and compliance as their own line.

Verdict: you’re in distribution platform territory, not “one more CI Mac” — artifact registry, policy, audit; outside this runbook.

Stage → action (decision loop)
Stage Do this now
Stage 1Stay on one Mac; tune build/upload windows
Stage 2Stop buying Macs; implement Model A + acceptance table
Stage 3Scale build horizontally; tighten upload monitoring
Stage 4Separate project: multi-region distribution, not Fastlane patches
Upgrade trigger (problem already happening)
If CI already shows “TestFlight randomly empty + multiple Mac runners + match on more than one node,” you’re in mandatory role-split territory — not the “buy another runner” phase. Stuck at Stage 2: fix lanes using the triage tree and Model B anti-patterns — no third Mac before the split.

Final verdict (this block is enough to act)

If all three are true, decide here — no need to reread the rest:

  • At least 2 Mac runners (or equivalent multi-node CI)
  • Fastlane chain with unstable TestFlight (match failed, processing stuck, sporadic missing builds)
  • match has run on more than one host

👉 Don’t buy more hardware.

👉 Three steps (start today):

  1. Stop match / sync_code_signing on every non-upload node (signing stays on upload only)
  2. Enforce cross-DC handoff of .ipa + manifest.json only (no .xcarchive rsync)
  3. Standardize RUN_ID + GIT_SHA + ipa_sha256; upload must verify before upload

If you can’t do those three: you’re still in Stage 2 (Model B). More hardware amplifies the failure; it doesn’t fix it. Fix Fastfile and workflow labels first, get the acceptance table green, then talk build capacity.

Upload machine Fastfile (Model A · excerpt)
lane :upload_only do |options|
  verify_run_id!(options) # manifest.json
  match(type: "appstore", readonly: true)
  upload_to_testflight(ipa: options[:ipa_path], api_key_path: "asc_api_key.json")
end
Change discipline
Don’t rotate match the week you split lanes. Certificate changes are a separate event: stop both lanes, rotate on upload only.

Quick FAQ

Can the build machine archive without Distribution signing? Project-dependent; we export on build an IPA for final App Store signing on upload (or build is compile-only and upload resigns). Non-negotiable: match on one host only.

match expiring soon? Rotate on upload only in a non-readonly window; pause build until profiles stabilize.

How is this different from the travel runbook? Travel runbook = Wi‑Fi on the road; this one = datacenter roles and Fastlane boundaries — copy the acceptance table as-is.

Series: Part ① Runbook: dual-cloud Mac + Fastlane triage (empty TestFlight / match failed / multi-Mac iOS CI). Coming: ② Build·Sign·Distribute architecture, ③ Model B deep dive; setup entry point TestFlight dedicated runner.

Further reading

Dual-node roles, fixed egress, and specs: Hong Kong plans and pricing. Still deciding whether you need a dedicated upload Mac? Start with the TestFlight deep dive.

Hashvps

Stuck at Stage 2? Role split first, then add Macs

Dedicated upload IPv4 for ASC allowlists; scale build nodes in APAC when Stage 3 is green.

View plans
Limited offer