Figment.so
Mac hostingWebsite hostingGuidesHow to usePricing

Build Signed IPAs with Fastlane Gym in CI

Raw xcodebuild -exportArchive commands spread flags across pipeline YAML, hide export options in a plist, and bury errors in multi-thousand-line logs. Fastlane's gym action (an alias for build_app) wraps the whole archive-and-export sequence in one readable config. This guide gives you a portable Gymfile, a Fastfile lane that asserts the IPA path, the match wiring for signing, and a clear recommendation on which hosted Mac to run it on.

for a single-app team doing Gym builds a few times a day, start with a Figment M-series Mac from $95/mo (exact chip, RAM, and disk confirmed in the quote because physical hardware is request-based). If you want the runner, backups, monitoring, and the initial CI pipeline wired for you, the Managed Build Mac plan at $249/mo bundles one M-series Mac with CI setup and 30 minutes of monthly support. The $29/mo virtual Intel macOS VM can run Xcode 26 or earlier and sign IPAs for supported Intel targets, but if your release target needs Xcode 27 or an iOS Simulator slice that is Apple‑silicon‑only, pick the M-series Mac. See /mac-hosting for plans and /mac-hosting/start to open a configuration request.

Why Gym Instead of Raw xcodebuild

Gym runs both steps in one pass: xcodebuild archive then xcodebuild -exportArchive. It applies xcpretty formatting so errors are readable, and it writes the raw xcodebuild log under ~/Library/Logs/gym/ for post-mortems — both confirmed in the gym docs. A Gymfile at the project root keeps your pipeline YAML short; no reviewer needs to parse a 20-flag xcodebuild invocation to understand what the build step does.

Install fastlane once per runner. On a hosted Mac, bundle it with your project so CI uses a pinned version:

# Project-local, reproducible
bundle init
bundle add fastlane
bundle exec fastlane --version

Gymfile Fields That Matter

The Gymfile is a Ruby DSL file at fastlane/Gymfile. Every field has a default, so you only specify what differs. The fields you will almost always set:

# fastlane/Gymfile

# Required: which scheme to build. Use the exact scheme from your project.
scheme(ENV.fetch("APP_SCHEME"))

# Use workspace if you have CocoaPods or multi-project setups
workspace(ENV.fetch("APP_WORKSPACE"))

# Build configuration (default: Release)
configuration("Release")

# Export method: "app-store", "ad-hoc", "development", "enterprise"
export_method("app-store")

# Where to write the IPA
output_directory(ENV["RUNNER_TEMP"] || "./build")

# Explicit IPA filename (default: scheme name)
output_name(ENV.fetch("APP_SCHEME"))

# Extra xcodebuild flags passed verbatim
xcargs("ONLY_ACTIVE_ARCH=NO")

# Keep xcpretty enabled for readable output
xcpretty_args("--color --simple")

Driving scheme and workspace from environment variables keeps this Gymfile reusable across projects — pick values that match your own Xcode project rather than hard-coding a placeholder. xcargs appends KEY=VALUE build settings directly to xcodebuild; use it when there is no dedicated Gymfile key.

The Fastfile Lane with an IPA Path Assertion

The Fastfile lane orchestrates signing (match), the build (gym), and an explicit check that the IPA landed where downstream steps expect it.

# fastlane/Fastfile
default_platform(:ios)

platform :ios do
  desc "Build a signed App Store IPA and assert the output file exists"
  lane :build_release do
    match(
      type: "app-store",
      readonly: is_ci,
      app_identifier: ENV.fetch("APP_BUNDLE_ID")
    )

    gym

    ipa_path = lane_context[SharedValues::IPA_OUTPUT_PATH]
    UI.user_error!("IPA_OUTPUT_PATH not set by gym") if ipa_path.nil?
    UI.user_error!("IPA not found at #{ipa_path}") unless File.exist?(ipa_path)
    UI.success("IPA produced at: #{ipa_path}")
  end
end

is_ci returns true when the CI environment variable is set, which GitHub Actions, Buildkite, and most CI platforms do automatically. Readonly match fetches existing certificates and profiles without generating new ones — the right default in CI, per the match docs.

The path assertion catches the case where gym exits zero but downstream steps (TestFlight upload, artifact upload) look at the wrong path. SharedValues::IPA_OUTPUT_PATH is the documented lane-context key for the generated IPA.

Using Match for Code Signing in CI

Match stores encrypted certificates and provisioning profiles in a Git repository, S3, or Google Cloud Storage. For Git, set the passphrase through the documented MATCH_PASSWORD environment variable so the decrypt step runs without a prompt. Put the repository URL in a Matchfile:

# fastlane/Matchfile
git_url("git@github.com:your-org/certificates.git")
storage_mode("git")
type("appstore")

In CI, provide only the secret:

MATCH_PASSWORD=<your-match-passphrase>

Keep the passphrase in your CI secret store (GitHub Actions encrypted secrets, Buildkite agent-level secrets, etc.) — not in the Fastfile or any file in source control. Match installs the profile and sets PROVISIONING_PROFILE_SPECIFIER, which gym picks up automatically.

Signing material moving to a new runner should transfer as an encrypted .p12 for the signing identity (certificate + private key) plus the .mobileprovision files on disk. Provisioning profiles are files, not Keychain items; copy them from ~/Library/MobileDevice/Provisioning Profiles/ rather than exporting from Keychain.

Portable Gymfile: GitHub Actions and Buildkite

The Gymfile has no CI-platform-specific syntax. The same file runs on GitHub Actions self-hosted runners, Buildkite agents, and a Figment Mac without edits. What differs is how the output directory and secrets are wired.

GitHub Actions:

# .github/workflows/build.yml
jobs:
  build:
    runs-on: [self-hosted, macOS]
    env:
      RUNNER_TEMP: ${{ runner.temp }}
      APP_SCHEME: MyApp
      APP_WORKSPACE: MyApp.xcworkspace
      APP_BUNDLE_ID: com.example.MyApp
      MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
    steps:
      - uses: actions/checkout@v4
      - run: bundle install
      - run: bundle exec fastlane build_release
      - uses: actions/upload-artifact@v4
        with:
          name: ipa-${{ github.sha }}
          path: ${{ runner.temp }}/MyApp.ipa

Buildkite:

# .buildkite/pipeline.yml
steps:
  - label: "Build release IPA"
    command: "bundle install && bundle exec fastlane build_release"
    env:
      RUNNER_TEMP: "build"
      APP_SCHEME: "MyApp"
      APP_WORKSPACE: "MyApp.xcworkspace"
      APP_BUNDLE_ID: "com.example.MyApp"
    agents:
      queue: "mac-runners"
    artifact_paths: "build/MyApp.ipa"

The Gymfile and Fastfile are identical in both cases; only the ambient environment and the artifact-upload step differ.

Debugging Gym Failures

When gym fails, read the raw xcodebuild log that gym wrote, not the xcpretty summary:

ls ~/Library/Logs/gym/
grep -nE "error:|note: Using|CodeSign" ~/Library/Logs/gym/MyApp-MyApp.log | head -40

Common failures and where to look:

SymptomWhere to look in the raw log
Signing identity not founderror: near CodeSign lines
Provisioning profile mismatchembedded.mobileprovision lines
Archive missing device sliceONLY_ACTIVE_ARCH or arm64 lines
Missing schemedoes not contain a scheme named
xcpretty hid the errorre-run with gym(disable_xcpretty: true)

For a one-off diagnostic run, pass verbose through to xcodebuild from the lane:

gym(verbose: true)

Leave it off by default; the output is large and slows log ingestion.

Picking a Figment Mac for Gym Builds — a Worked Choice

A quick worksheet for a team deciding between Figment plans for this exact task:

  • Solo developer, one app, a few builds per day, happy to wire CI themselves: Figment M-series from $95/mo (quoted chip/RAM/disk). Twelve months at that entry price totals $1,140 before any term discount — ask for a term quote rather than assuming one.
  • Small team, wants the runner installed and the Fastlane pipeline above wired by someone else: Managed Build Mac $249/mo includes one M-series Mac, CI setup, backups, monitoring, and 30 minutes of monthly support — the 30 minutes is a cap on included support time, not an SLA.
  • Two concurrent apps or parallel PR builds: Managed Mac Team $999/mo includes two M-series Macs, two CI runners, team setup, backups, monitoring, and one hour of monthly support.
  • Intel-only legacy target on Xcode 26 or earlier, no ARM simulator needs: the $29/mo virtual Intel macOS can carry that build. Confirm your Xcode/macOS pair in the Xcode release notes before committing; some simulators and visionOS require Apple silicon.

If you are comparing against other hosts for an equivalent M4 class, check their current pages directly: CloudMac publishes a $99.99/month M4/16 GB/256 GB offer and a $999/year annual on cloudmac.org/pricing and cloudmac.org (effective $83.25/month if you prepay the year, with the 256 GB tier marked Reserve now and larger tiers Fully reserved at the time of writing). Figment's $95 entry is an unspecified M-series — a lower starting price, not a like-for-like M4/16 GB match — and the Managed Build and Managed Team plans are the differentiator when you want the CI pipeline delivered, not just the Mac. Open a quote at /mac-hosting/start with your Xcode version, scheme list, and concurrency target, and we will size the plan to it.

Related guides:


Subscribe for Figma design and website publishing guides.


Figment.so

Contact

Twitter

Website pricing

Compute index

About

Privacy

Terms