
For a Flutter iOS pipeline that archives and ships to TestFlight, request a Figment M-series Mac from $95/month at /mac-hosting/start and register it as a GitHub self-hosted runner. The exact M-series chip, RAM and disk are confirmed in the quote; physical plans collect requests until supply is confirmed. If you want someone else to own runner setup, backups and monitoring, the Managed Build Mac plan at $249/month bundles that work on one M-series Mac with 30 minutes of monthly support. The $29 virtual Intel macOS VM (2 vCPU, 8 GiB RAM, 80 GiB disk) can host an Intel-compatible Xcode 26-or-earlier toolchain and cross-compile iOS targets, but Dart's arm64 AOT compile is faster and the long-term toolchain path belongs on Apple silicon — Xcode 27 is Apple-silicon-only per Apple's porting notes.
Flutter iOS CI stitches four tools together on one Mac — Flutter, CocoaPods, Ruby, and Xcode — in a specific order, documented in the primary Flutter iOS deployment guide. The extra CI problem is Flutter SDK drift between developers and runners. This page solves that with FVM (Flutter Version Management), commits a signing export plist, and ends on a pilot upload to TestFlight.
flutter build ipa is not one command; it drives several underneath:
flutter pub get fetches Dart packages and writes the Flutter-engine Podfile entries.pod install resolves the CocoaPods graph for the iOS plugins.xcodebuild archive builds the generated Runner.xcworkspace.xcodebuild -exportArchive produces the signed IPA using your ExportOptions.plist.So the Mac needs Xcode, CocoaPods, Ruby (CocoaPods and Fastlane), and Flutter. This workflow installs Flutter on the runner via FVM so the only preinstalled requirements are Xcode and a Ruby toolchain. On a Figment M-series Mac, the Dart AOT compile runs native arm64, which is where most of the per-build time goes on real apps.
FVM reads .fvmrc at the repo root. Commit it so every developer and every CI run compiles against the same release:
{
"flutter": "3.24.3",
"flavors": {}
}
Install FVM on the runner from the current tap. Per the FVM installation docs, the tap is btwld/fvm (migrated from the older leoafarias/fvm):
- name: Install FVM
run: |
brew tap btwld/fvm
brew install btwld/fvm/fvm
- name: Install pinned Flutter
run: fvm install
- name: Expose fvm flutter on PATH
run: echo "$(fvm flutter --version --machine >/dev/null; fvm list --path 2>/dev/null || true)" >> /dev/null
After fvm install, call Flutter as fvm flutter for every subsequent step. FVM proxies the pinned version so you do not have to export a path manually.
Flutter plugins that expose native iOS code are written into ios/Podfile by flutter pub get. If pod install runs first, CocoaPods either fails or reuses a stale cached graph and ships the wrong plugin version.
- name: Dart dependencies
run: fvm flutter pub get
- name: CocoaPods cache
uses: actions/cache@v4
with:
path: ios/Pods
key: pods-${{ runner.os }}-${{ hashFiles('ios/Podfile.lock') }}
restore-keys: pods-${{ runner.os }}-
- name: CocoaPods install
run: cd ios && pod install
Keying the cache on Podfile.lock means any plugin bump invalidates it; unchanged graphs skip the network.
Commit ios/ExportOptions.plist. A minimal App Store version:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key><string>app-store</string>
<key>teamID</key><string>YOUR_TEAM_ID</string>
<key>uploadBitcode</key><false/>
</dict>
</plist>
Then:
fvm flutter build ipa --release \
--export-options-plist=ios/ExportOptions.plist
The IPA lands in build/ios/ipa/. The signing identity and provisioning profile must be in place before this step — handle that with Fastlane Match against a private, encrypted certificates repo. Never export your whole shell environment to disk, and never import provisioning profiles into Keychain; profiles are files Xcode reads from ~/Library/MobileDevice/Provisioning Profiles/.
# .github/workflows/flutter-ios.yml
name: Flutter iOS CI
on:
push:
branches: [main, 'release/**']
pull_request:
branches: [main]
jobs:
build-ios:
runs-on: [self-hosted, macOS, figment] # label your runner to match
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
bundler-cache: true
- name: Install FVM
run: |
brew tap btwld/fvm
brew install btwld/fvm/fvm
- name: Install pinned Flutter
run: fvm install
- name: Dart dependencies
run: fvm flutter pub get
- uses: actions/cache@v4
with:
path: ios/Pods
key: pods-${{ runner.os }}-${{ hashFiles('ios/Podfile.lock') }}
restore-keys: pods-${{ runner.os }}-
- name: CocoaPods install
run: cd ios && pod install
- name: Sync signing (Fastlane Match, read-only)
run: bundle exec fastlane match appstore --readonly
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }}
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.ASC_KEY_ID }}
APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
APP_STORE_CONNECT_API_KEY_CONTENT: ${{ secrets.ASC_PRIVATE_KEY }}
- name: Build IPA
run: |
set -euo pipefail
fvm flutter build ipa --release \
--export-options-plist=ios/ExportOptions.plist
- name: Upload to TestFlight
run: |
set -euo pipefail
IPA=$(ls build/ios/ipa/*.ipa | head -1)
bundle exec fastlane pilot upload --ipa "$IPA"
env:
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.ASC_KEY_ID }}
APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
APP_STORE_CONNECT_API_KEY_CONTENT: ${{ secrets.ASC_PRIVATE_KEY }}
set -euo pipefail makes each script step stop on the first failure instead of letting a broken archive march on to TestFlight. This is a reference workflow to adapt, not a measured benchmark.
Here is a worked worksheet, not a quote. Your team runs 40 Flutter iOS builds a week. On GitHub-hosted macOS at $0.062/minute, a 20-minute build costs $1.24 per run, or roughly 40 × 4.33 × $1.24 ≈ $215/month before any queueing. Figment Managed Build Mac is $249/month, flat, with CI setup, backups, monitoring and 30 minutes of monthly support on one M-series Mac. If you expect two 20-minute builds per push and the pipeline grows past ~215 runs per month, the managed plan is the simpler line-item; if volume is lower, the self-hosted M-series at $95/month plus your own setup time is the lower cash cost. The sibling guide GitHub-hosted vs self-hosted Mac runner costs steps through break-even with queue time included.
.fvmrc committed with the target Flutter version.ios/ExportOptions.plist committed with the correct teamID.MATCH_PASSWORD, MATCH_GIT_BASIC_AUTHORIZATION, ASC_KEY_ID, ASC_ISSUER_ID, ASC_PRIVATE_KEY.runs-on list.Request a configured Mac at Figment Mac hosting or start the quote at /mac-hosting/start.
Related guides: Send Slack alerts for iOS CI build status · GitHub-hosted vs self-hosted Mac runner costs · Remote Xcode setup on Windows