Figment.so
Mac hostingWebsite hostingGuidesHow to usePricing

Migrate from Xcode Cloud to a Self-Hosted Mac

Xcode Cloud's 25 free compute hours disappear fast on a team with several active branches. Apple's published paid tiers step up to US$49.99/month for 100 hours, US$99.99/month for 250 hours, US$399.99/month for 1,000 hours and US$3,999.99/month for 10,000 hours — see the Xcode Cloud subscription page. A team running ten 15-minute builds a day consumes the 100-hour tier in about ten working days. A self-hosted Mac on Figment is a flat monthly fee rather than a metered build-minute clock: the $29 virtual Intel macOS runner handles Intel-compatible Xcode workloads (Xcode 26 or earlier), and M-series starts at $95/month with the exact chip, RAM and disk confirmed in the quote. If you are already past 100 Xcode Cloud hours a month, price the Figment option against your current tier using the worksheet below, then follow the mapping table to cut over without losing a release cycle. Primary references used throughout: Apple's Xcode Cloud documentation and Configuring your first Xcode Cloud workflow.

Xcode Cloud Workflow Anatomy

Every Xcode Cloud workflow has the same shape, and replicating it requires naming each piece:

  1. Start condition — branch push, pull request, tag, schedule or manual run.
  2. Environment — selected Xcode version, macOS image and environment variables.
  3. Build action — compiles the project; can run tests inline.
  4. Test action — executes a chosen .xctestplan against simulator or device destinations.
  5. Post-actions — TestFlight distribution, App Store submission, Slack or email notifications.

Workflows are configured in Xcode's Report navigator or in App Store Connect; there is no YAML file on disk. Migration is deliberate mapping, not a file copy.

Migration Mapping Table

Mark each row done when the self-hosted command produces output that matches the Xcode Cloud build.

Xcode Cloud actionSelf-hosted equivalentNotes
Start: branch pushCI webhook on pushGitHub Actions, Buildkite, GitLab CI, Jenkins
Start: pull requestCI pull_request triggerScope with -only-testing: for PR test subsets
Start: tagCI tag triggerUse for release builds
Start: scheduleCI cron triggerNightly builds, regression sweeps
Environment: Xcode versionDEVELOPER_DIR pointing at the installed Xcode bundleSee the sibling guide below on multiple Xcode versions
Buildxcodebuild build -scheme … -destination … or fastlane gymgym wraps archive and export
Test (unit)xcodebuild test -scheme … -testPlan …Pass the committed .xctestplan path
Test (UI)xcodebuild test -scheme … -destination "platform=iOS Simulator,…"Destination must match an installed simulator
Archivexcodebuild archive -scheme … -archivePath …Required before export and upload
Export IPAxcodebuild -exportArchive -archivePath … -exportOptionsPlist … or gymKeep exportOptionsPlist in source control
Post: TestFlightfastlane upload_to_testflightAuthenticate with an App Store Connect API key
Post: App Storefastlane upload_to_app_storeMetadata upload is optional
Post: Slackfastlane slack with webhook URLStore SLACK_URL in CI secrets
Post: emailCI notification plugin on the runnerMost CI platforms ship a built-in email step

Hardware note: Apple silicon is required for Xcode 27, visionOS simulators and ARM-only execution. Intel-compatible targets built with Xcode 26 or earlier run on the $29 virtual Intel macOS runner; see Apple's porting guide for the exact boundary. Choose M-series when your matrix includes Xcode 27, ARM execution or GPU-heavy tests.

Replicate Test Plans

Xcode Cloud reads the .xctestplan files you pick in its UI. xcodebuild reads the same files:

xcodebuild test \
  -workspace MyApp.xcworkspace \
  -scheme "$SCHEME" \
  -testPlan "$TEST_PLAN" \
  -destination "platform=iOS Simulator,name=iPhone 16,OS=18.0" \
  -resultBundlePath "$RUNNER_TEMP/TestResults.xcresult"

$SCHEME and $TEST_PLAN are your project's real values — do not hard-code a scheme name that does not exist in the workspace. If the test plan is not yet in source control, export it from Xcode (Product → Test Plan → Export) and commit it before migration. Confirm destinations with:

xcrun simctl list devices available

A mismatched destination string is the most common reason a parity run diverges from Xcode Cloud.

Re-create Post-Actions in Fastlane

The two post-actions that matter for most teams are TestFlight upload and Slack notification.

# fastlane/Fastfile
default_platform(:ios)

platform :ios do
  desc "Archive, upload to TestFlight, notify Slack"
  lane :beta do
    gym(
      scheme: ENV.fetch("SCHEME"),
      workspace: ENV.fetch("WORKSPACE"),
      export_method: "app-store",
      output_directory: ENV["RUNNER_TEMP"] || "./build",
      output_name: "MyApp.ipa"
    )

    upload_to_testflight(
      api_key_path: ENV.fetch("ASC_API_KEY_JSON"),
      skip_waiting_for_build_processing: true
    )

    slack(
      message: "TestFlight build uploaded",
      channel: "#ios-releases",
      slack_url: ENV.fetch("SLACK_WEBHOOK_URL")
    )
  end
end

The api_key_path value points at an App Store Connect API key JSON that Fastlane reads at runtime; generate the .p8 in App Store Connect under Users and Access → Integrations, then assemble the JSON per the Fastlane App Store Connect API docs. Keep the .p8 and the JSON in your CI secret manager — never commit them, and never echo them into shell history. Signing certificates should be stored as encrypted .p12 files with the private key; provisioning profiles are regular .mobileprovision files copied into ~/Library/MobileDevice/Provisioning Profiles/. Neither is exported from Keychain by a CI step.

Run Both Pipelines in Parallel

Do not disable Xcode Cloud until you have confirmed parity on at least three real builds.

  1. Week 1. Stand up the self-hosted runner, install the pinned Xcode, and run the lane manually against a feature branch. Compare IPA size, build number and test counts against the matching Xcode Cloud run.
  2. Week 2. Trigger both pipelines on every push to develop. Diff the artifacts and the .xcresult summaries.
  3. Week 3. Route the release branch exclusively to the self-hosted pipeline. Leave the Xcode Cloud workflow enabled but idle.
  4. After one clean release. Disable the Xcode Cloud workflow. Wait one billing cycle before deleting it so you can roll back.

Parallel operation costs both Xcode Cloud hours and the Figment monthly fee for one or two weeks. That overlap is cheaper than a failed release caused by an undiscovered gap.

Cost Worksheet: Xcode Cloud vs a Self-Hosted Mac

Use your own numbers; the example below is a moderate-volume team.

  • 20 builds/day × 5 days × 4 weeks = 400 builds/month
  • 15-minute average = 100 compute hours/month exactly
  • Any retries, parallel UI test shards or long archive steps push over 100 hours, forcing the 250-hour tier.
MetricXcode Cloud 250-hr tierFigment M-series entry
Monthly cost$99.99 (Apple)from $95 (entry price; exact chip/RAM/disk set in the quote)
Billing modelMetered compute hours, unused hours expireFlat monthly fee, not metered
Per-build cost at 400 builds$0.25 ($99.99 ÷ 400)$0.24 ($95.00 ÷ 400)
Next step up$399.99 for 1,000 hoursConfirm larger spec in the quote

The entry-price per-build numbers are close, but they do not represent equivalent hardware. Xcode Cloud is Apple's shared managed infrastructure; the Figment entry is a single-tenant Mac whose spec is confirmed at quote time. The practical advantage of self-hosting appears at the next step up: once build volume exceeds the 250-hour tier, Xcode Cloud jumps to $399.99/month for 1,000 hours, while a Figment M-series stays on a flat monthly fee for the spec you request. Smaller teams under 100 compute hours a month may find Xcode Cloud's $49.99 tier cheaper than any dedicated Mac; in that range the $29 virtual Intel macOS runner is the realistic Figment comparison for Intel-compatible targets, and Xcode Cloud remains the simpler option for ARM-only builds.

Disable the Xcode Cloud Workflow After Sign-Off

Once a self-hosted release has shipped cleanly, open App Store Connect → Xcode Cloud → Workflows, select the workflow and disable or delete it. The same workflow is also reachable from Xcode's Report navigator; UI labels shift between Xcode point releases, so follow Apple's current Xcode Cloud documentation rather than screenshots. After one or two billing cycles with no Xcode Cloud usage, lower or cancel the subscription tier.

Migration Checklist

  • .xctestplan files committed to source control
  • exportOptionsPlist or gym export_method set for app-store export
  • App Store Connect API key stored in CI secrets, not in the repo
  • Encrypted signing .p12 and .mobileprovision files installed on the runner
  • Slack webhook URL stored in CI secrets
  • Simulator destinations verified with xcrun simctl list devices available
  • Self-hosted and Xcode Cloud pipelines run in parallel on ≥ 3 builds
  • IPA size, build number and test pass/fail counts match between pipelines
  • Xcode Cloud workflow disabled after the first clean self-hosted release
  • Xcode Cloud tier reduced or cancelled after one idle billing cycle

Ready to size the Mac? Review the plans at /mac-hosting and begin at /mac-hosting/start.

Related guides:


Subscribe for Figma design and website publishing guides.


Figment.so

Contact

Twitter

Website pricing

Compute index

About

Privacy

Terms