This article does one thing
- Build the Flutter iOS CI = Control Plane + Execution Plane + Cache Plane three-plane mental model.
- Provide a macos-latest vs. self-hosted decision matrix — settle it in 5 minutes.
- Route deep implementation to 5 topic sub-articles — enter at your pain point.
The Three-Plane CI Model
Before looking at any specific tool, establish one equation — it determines which layer to fix:
Flutter iOS CI =
Control Plane (GitHub Actions: triggers · approvals · Secrets)
+ Execution Plane(Mac mini M4 : native iOS build · signing · env consistency)
+ Cache Plane (Local SSD : persistent deps and build artifact storage)
Three corollaries that drive every decision in this series:
- The control plane doesn't run code. Optimizing workflow YAML doesn't speed up builds. Reducing build minutes requires replacing the execution plane.
- The execution plane determines reproducibility. A shared pool (
macos-latest) has Xcode versions that update with GitHub; a self-hosted machine has versions you pin. - The cache plane determines speed. Self-hosted speed gains don't come from CPU — they come from local SSD replacing per-job remote cache uploads and downloads.
Why Flutter iOS CI Needs a Dedicated macOS Execution Plane
Flutter can handle Android builds and unit tests on Linux, but iOS builds and App Store releases can only run on macOS — a hard constraint from the Apple toolchain, not from Flutter itself.
So every Flutter team eventually faces an architecture choice: where does the iOS leg run? Three common friction points drive teams away from macos-latest:
- Cost: GitHub-hosted macOS runners carry a significantly higher per-minute rate than Linux. Cold iOS builds take 20–30 minutes; the bill scales quickly with build frequency.
- Environment consistency: Hosted image Xcode versions update with GitHub — you cannot pin them. "Passes locally, fails on CI" becomes a recurring debugging tax.
- Build speed: iOS dependencies are large. Hosted runners re-upload and re-download the cache every job. A self-hosted machine on local SSD eliminates that bottleneck entirely.
"Bring the macOS execution plane back inside a controlled boundary" is the central architectural decision here — keep the control plane in GitHub (PR triggers, review gates, Secrets), and swap only the execution layer for a tenant-dedicated Mac mini M4.
Official references: GitHub self-hosted runners docs · Flutter CD guide
Architecture Overview
Map the whole pipeline to the three-plane model — when something breaks, identify the plane first, then go to the right topic article:
| Plane | Owns | Does NOT own | Failure signals |
|---|---|---|---|
| Control Plane | Trigger rules, permissions, Secrets injection, artifact upload | Does not execute any build code | Job never triggers / permission error / PR gate broken |
| Execution Plane | Native iOS build, pinned Xcode version, signing chain | Does not manage triggers or Secrets source | Build env drift / signing failure / runner offline |
| Cache Plane | Cross-job persistence of dependencies and build artifacts | Does not decide build triggers or execution env | Hot build ≈ cold build / disk warning / random failures |
Every Flutter iOS CI failure maps to one of these three rows. Identify the plane correctly and the fix path becomes obvious.
Decision Matrix: When Is Migration Worth It?
Self-hosted isn't the default answer. Use this matrix to make the call in a 5-minute team review:
| Signal | Keep macos-latest | Migrate to self-hosted |
|---|---|---|
| Build frequency | < 40 iOS builds per month | > 40/month, or multiple times daily |
| Env stability | Not sensitive to Xcode minor versions | Need to pin Xcode / SDK version |
| Build speed | Hot build < 8 min is acceptable | Hot build consistently > 8 min, slowing iteration |
| Release pipeline | No App Store signing requirement | App Store / TestFlight release pipeline in use |
| Ops appetite | Zero-ops preferred | Willing to trade one maintenance window/month for control |
Rule of thumb: Hit 3 or more "migrate" signals — self-hosted typically pays back within 2–4 weeks (machine rental vs. saved hosted macOS minutes).
Topic Series: Enter at Your Pain Point
This article only establishes architecture awareness and the migration decision. Each execution-layer topic has a dedicated deep-dive article:
| Topic | Core question answered | Plane | Link |
|---|---|---|---|
| ① Runner Setup | How to keep a Mac mini online, schedulable, and securely isolated | Execution | Coming soon |
| ② Three-Layer Cache Model | Why iOS builds can't speed up, and how to fix it with local SSD | Cache | Cache invalidation deep-dive → |
| ③ Signing & Release | How to complete iOS signing and App Store upload in unattended CI | Execution | Coming soon |
| ④ Workflow Design | How to structure Android/iOS job split, path filtering, and concurrency | Control | Coming soon |
| ⑤ Ops & Stability | Why CI breaks, how to recover from runner outages, how to keep disk from filling up | Execution + Cache | Coming soon |
Choose Your Entry Path
- Starting from scratch: Use this article to confirm architecture direction → topic ① "Runner Setup" to get the machine online → topic ② "Cache Model" to tune speed → topic ③ "Signing" to wire the release pipeline.
- Runner already running but iOS build is slow: Go directly to topic ② "Cache Invalidation Deep-Dive".
- Signing or TestFlight upload is stuck: Go to topic ③ "Signing & Release".
- Runner goes offline / builds randomly fail: Go to topic ⑤ "Ops & Stability".
- Migrating from Bitrise or other cloud CI: See Bitrise vs. self-hosted cloud Mac decision guide for a migration cost comparison.
Need a Mac mini to run this architecture on?
kvmboot provides tenant-dedicated Mac mini M4 (16 GB / 24 GB), delivered over SSH/VNC, billed by day, week, or month. Your control plane stays in GitHub; the execution plane moves to this machine. The three-plane model in this article typically takes Day 0 to provision, Day 1 to connect the CI pipeline, and Day 2 to validate signing — with consistent, pinned Xcode versions from the first build.
Specs and plans: View plans · Configuration options