開發
Mac mini M1 GitHub Actions 自託管 Runner:完整 Flutter CI/CD 設定指南(2026)
在 Mac mini M1 上為 Flutter 設定 macOS ARM64 GitHub Actions runner:launchd、Xcode、驗證 workflow、疑難排解、安全與成本。
為甚麼用 Mac mini 做 Flutter CI/CD?
Mac mini M1 可以把一部閒置的 Apple Silicon 電腦,變成可重複驗證 Flutter 應用程式、編譯其 iOS 目標的地方。關鍵分別在於擁有權:何時更新 Xcode、專案使用哪個 Flutter SDK、哪些工作可以接觸這部機器,都由你決定。當原生外掛或較舊的發佈分支依賴特定工具鏈時,這種控制權特別有價值。
本指南為私人儲存庫建立一個小型、受信任的 CI runner。最終成果是一個手動觸發的工作:檢查格式、分析 Dart、執行 Flutter 測試,並編譯一個未簽署的 iOS 應用程式。它不會建立發佈憑證、上載 IPA,亦不會發佈 App Store 版本。這些屬於不同的交付階段,各有不同的權限和驗收檢查。
下文所有路徑、機器名稱、容量建議和 workflow 選擇都只是示例設定,並非 AiLane 正式環境基礎設施的描述,亦不是實測的建置效能。先從一個 runner 和一個工作開始,量度自己的排隊時間、建置時間、記憶體壓力和維護工作量,然後才考慮擴展。
需求與合理的起步設定
為 runner 使用一個專用的 macOS 帳戶,其主目錄不應包含個人文件或無關的憑證。M1 是 ARM64 機器;只要工具提供,就選擇原生 ARM64 下載。一個示例起點是 16 GB 記憶體、足夠容納 Xcode 和多個建置工作區的 SSD 空間、有線網絡,以及一份有文件記錄的復原程序。這些是規劃選擇,不是最低要求或基準測試建議。
- 選擇一個同時受 runner 和你的應用程式所需 Xcode 版本支援的 macOS 版本。單看 runner 的作業系統最低要求,並不能確立 iOS 建置相容性。
- 註冊 runner 需要儲存庫擁有者或合適的組織管理員權限。購買或設定硬件前,先檢查組織的限制。
- 容許 runner 經對外 HTTPS 連接 GitHub 和你的套件來源。註冊 runner 不需要開放任何公開的入站端口。
- 為重新開機、登入工作階段、更新和 FileVault 復原,預先安排實體接觸或經批准的遙距管理途徑。停在磁碟解鎖畫面的機器無法接收工作。
GitHub 託管與自託管 runner 比較
GitHub 託管的 runner 省去了大部分機器管理工作。若只是偶爾建置 iOS,這種簡單可能比保留本機快取更有價值。自託管的 Mac 讓你控制已安裝的工具和本機儲存空間,但每個成功的工作都會令機器留下某種狀態,下一個工作可能會繼承。請把這種持久性同時視為營運責任和安全邊界。
比較完整的 workflow,而不是比較 CPU 名稱。已預熱的依賴快取可能有助重複建置,但在其他情況下,大型 checkout、緩慢的網絡或串行執行的測試套件可能才是主要瓶頸。單一機器的佇列亦有上限:兩個同時到來的發佈,不能都假設自己獨佔其 keychain、模擬器、端口或工作檔案。
首次安裝時,把 Linux 和 Apple 的工作分開。GitHub Actions 的 Docker container action 和 service container 需要 Linux runner。在 Mac 上安裝 Docker Desktop,並不會令使用 Linux container action 的工作在 macOS 上獲得支援。把這些工作交給 Linux runner,原生 Apple 建置則留在 Mac 上。
在 GitHub 建立儲存庫 runner
打開你的私人儲存庫,依次選擇 Settings、Actions、Runners,然後選 New self-hosted runner。選擇 macOS 和 ARM64。GitHub 會按此選擇產生下載、驗證、解壓和註冊指令。請使用設定當時你的儲存庫所顯示的指令;不要從教學文章抄襲舊的 runner 壓縮檔網址或註冊 token。
註冊頁面要保密。其 token 有時限,只屬於這次設定操作。若註冊逾時,請重新產生指示,不要把 token 存放在儲存庫、shell script、截圖或 issue 裏。使用清晰的 runner 名稱,例如 mac-mini-m1-ci,並保留預設的平台標籤。自訂標籤可以描述某項專用能力,但標籤並不是授權控制。
下載並註冊 macOS ARM64 runner
以將會擁有 runner 的帳戶登入。選擇一個位於「桌面」、「文件」和「下載」以外的永久資料夾,令一般工作區存取不必依賴受保護資料夾的權限提示。本例中 runner 位於 ~/actions-runner。在該資料夾內執行 GitHub 產生的下載和完整性檢查指令,解壓檔案,再執行產生的 ./config.sh 指令。
mkdir -p "$HOME/actions-runner"
cd "$HOME/actions-runner"
# 執行你的 GitHub 儲存庫 runner 設定頁面
# 目前提供的下載、驗證、解壓和 config 指令。
# 註冊完成後:
./run.sh等待前景程序開始監聽工作,而 GitHub 顯示 runner 在線。確認其標籤包括 self-hosted、macOS 和 ARM64。這次首個前景連線,能把註冊和網絡問題與之後的服務問題分開。啟動服務前,先用 Control-C 停止它;不要讓兩個監聽程序爭用同一個 runner 安裝。
不要把已註冊的 runner 目錄複製到另一部主機重用。每個 runner 都應有自己的註冊和工作資料夾。若下載或校驗和失敗,請停下來調查原始來源和網絡路徑;移除完整性檢查並不是復原方法。
以 launchd 服務執行 runner
在已註冊的 runner 目錄內,以 runner 帳戶執行官方服務輔助腳本。macOS 版輔助腳本不可用 sudo 執行。它會為該使用者建立一個 LaunchAgent;使用 sudo 的 Linux 服務示例與這些指令並不通用。
cd "$HOME/actions-runner"
./svc.sh install
./svc.sh start
./svc.sh statusLaunchAgent 屬於某個使用者工作階段。不要把磁碟上的 plist 等同於正在運行的監聽程序,亦不要假設機器重新開機後,合適的使用者工作階段已經回來。重新開機或登出後,檢查擁有帳戶、其 launchd 工作階段,以及 GitHub 的在線狀態。若 workflow 涉及模擬器或其他圖形介面服務,請明確驗證所需的圖形工作階段。單靠 SSH 登入,並不能證明圖形環境已經就緒。
輔助腳本會顯示已安裝的服務路徑,並把該 plist 路徑記錄在 .service。讀取其 Label 欄位,就可以集中診斷,毋須猜測其他無關的服務名稱。請以同一個已登入的使用者執行這項檢查。若 GUI domain 不存在或找不到該 label,先調查工作階段和服務安裝,才考慮修改 plist。
cd "$HOME/actions-runner"
service_plist="$(cat .service)"
service_label="$(/usr/libexec/PlistBuddy -c 'Print :Label' "$service_plist")"
launchctl print "gui/$(id -u)/$service_label"
# 有計劃的維護,須待進行中的工作完成後:
./svc.sh stop
# 機器就緒後再次啟動:
./svc.sh start保留一份書面的重新開機測試:機器可連線、runner 使用者工作階段就緒、服務運行中、runner 在線,然後完成一個真實的 CI 工作。不要只為了令測試通過而啟用自動登入或削弱磁碟加密。選擇一個符合這部機器安全要求的復原政策。
安裝 Flutter、Xcode 與原生依賴
從 Apple 安裝 Xcode,並選擇你的專案支援的版本。以機器擁有者身分,以互動方式完成首次啟動設定並閱覽授權條款。使用完整的 Xcode developer 目錄,而不只是獨立的 Command Line Tools 目錄。這些設定操作可能需要管理員權限,應在機器配置期間進行,而不是在每個 CI 工作中執行。
sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
sudo xcodebuild -runFirstLaunch
sudo xcodebuild -license
xcodebuild -version
xcode-select -p
xcodebuild -downloadPlatform iOS按照 Flutter 官方指示安裝 Apple Silicon 版 Flutter SDK。示例 workflow 預期已批准的 SDK 位於 ~/development/flutter。在儲存庫的開發文件中記錄其版本,並有計劃地更新。不要把 flutter upgrade 放進驗證工作:這樣做會在工作本應檢查某個特定版本時,改變工具鏈本身。
export PATH="$HOME/development/flutter/bin:/opt/homebrew/bin:$PATH"
command -v flutter
flutter --version
dart --version
flutter doctor -v
flutter precache --ios若你的專案或外掛使用 CocoaPods,請按照 CocoaPods 官方指南安裝。優先使用受管理的 Ruby 環境,而不是改動 Apple 的系統 Ruby。保持 Gemfile、Gemfile.lock 和 Podfile.lock 與專案一致。使用 Bundler 的專案必須讓 Flutter 建置使用其鎖定的 bundle;使用 Swift Package Manager 的專案應遵循該專案的整合方式,而不是單單因為教學提到 CocoaPods 就加入 Podfile。
ruby --version
command -v pod
pod --version按目標平台檢查 flutter doctor 的輸出。Android 工具鏈的警告不代表 iOS 失敗,但缺少 Xcode 元件就會。服務不一定會載入你的互動 shell 設定;請在真實工作中驗證工具路徑。把所需的 PATH 項目寫進 workflow 或受維護的配置設定。只授予實際執行工作的程序所需的特定 macOS 權限,而不是大範圍啟用「全磁碟取用權限」。
完整的 Flutter 驗證 workflow
把這個示例儲存為 .github/workflows/flutter-mac.yml,放在一個預設分支為 main、Flutter 專案位於儲存庫根目錄的私人儲存庫。它刻意只能從 main 手動觸發。請審查並保護該分支,限制誰可以修改 workflow,並只觸發你信任的程式碼。若你的儲存庫有所不同,請明確調整分支和工作目錄。
name: Flutter Mac validation
on:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: flutter-mac-validation
cancel-in-progress: false
jobs:
validate:
if: github.ref == 'refs/heads/main'
runs-on: [self-hosted, macOS, ARM64]
timeout-minutes: 45
defaults:
run:
shell: bash
steps:
- name: Check out trusted source
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Configure installed toolchain
run: |
echo "$HOME/development/flutter/bin" >> "$GITHUB_PATH"
echo "/opt/homebrew/bin" >> "$GITHUB_PATH"
- name: Record toolchain
run: |
uname -m
xcodebuild -version
flutter --version
flutter doctor -v
- name: Resolve dependencies
run: flutter pub get
- name: Check formatting
run: dart format --output=none --set-exit-if-changed .
- name: Analyze
run: flutter analyze
- name: Test
run: flutter test
- name: Compile unsigned iOS application
run: flutter build ios --release --no-codesign示例把 checkout 鎖定在其 v6.0.2 版本的完整 commit SHA。採用前,請核實所連結的版本及其 runner 版本要求,日後亦要審查對這個鎖定的更新。不可變的引用可防止移動的 tag 悄悄改變 action;但它不能取代審查 action 的實際行為,亦不能取代保持 runner 相容。
三個標籤是交集:runner 必須全部符合。concurrency group 只會在這個儲存庫內把此 workflow 串行執行;它並不是跨其他儲存庫或 runner 註冊的主機級鎖。先在 Mac 上只用一個註冊。若日後共用這部主機,在並行執行使用模擬器、keychain 或固定端口的工作之前,先制定統一的資源鎖政策。
最後一條指令在不簽署的情況下編譯。它既不能證明裝置可以安裝該 app,亦不能證明 App Store Connect 會接受它。正式簽署需要相符的 bundle identifier、開發者團隊、entitlements、憑證及私鑰、provisioning profile 或經批准的自動簽署設定,以及安全的 keychain 存取。發佈還需要 archive/export 設定、上載授權、Apple 處理,以及另外的發佈批准。
執行並驗證首個工作
按你平常的審查流程,把 workflow commit 到預設分支。打開 Actions,選擇 Flutter Mac validation,按 Run workflow 並選擇 main。留意工作是在排隊、已分配、運行中還是已完成。綠色的 runner 狀態只證明有監聽程序可用;工作結果才是工具鏈能夠執行你專案的證據。
- 確認工作在預期的 Mac 上執行,並報告 arm64、預期的 Xcode 和已批准的 Flutter 版本。
- 閱讀每個步驟的結果。被跳過的建置、被取消的工作,或從未取得 runner 的工作,都不算成功編譯 iOS。
- 保留 commit SHA、執行網址、工具鏈輸出和建置結果。不要在記錄中儲存 token、簽署材料或私人依賴的憑證。
- 在一次有計劃的服務重新啟動後再做一次。確認同一個受信任的版本,在沒有互動 shell 提供隱藏環境設定的情況下,仍然可以運作。
若示例在格式或分析步驟失敗,請透過正常審查修正所報告的專案問題。不要為了令安裝看似完成而移除這些檢查。若因應用程式需要 flavor 或額外建置參數而編譯失敗,請記錄這些要求並調整最後一條指令,但不要在基本驗證中引入發佈憑證。
更新與監察
runner 應用程式通常會自行更新,但這不會更新 macOS、Xcode、Flutter、Ruby、CocoaPods 或你的應用程式依賴。請分開記錄和維護這些項目。簡單的設定可保留 runner 自動更新;若想手動管理 runner 更新,請先參閱 GitHub 的更新政策。
在機器閒置時安排工具鏈維護。記錄新舊工具版本,盡可能每次只更新一個元件,執行已知的驗證 workflow,並保留復原途徑。即使 runner 本身一直在線,Xcode 更新仍可能改變 SDK 的可用性,或需要重新進行首次啟動準備。
監察排隊時間、服務可用性、剩餘磁碟空間和實際的 workflow 失敗。runner 日誌位於 runner 目錄的 _diag 資料夾;請檢查失敗時間對應的 Runner 和 Worker 日誌。分享前先遮蓋敏感資料。為工作區、快取和日誌訂立保留政策,但不要在另一個工作正在使用時刪除共用的快取或建置目錄。
常見錯誤與如何收窄範圍
- 工作一直排隊:逐一比較 runs-on 標籤與 runner 的標籤,檢查儲存庫或 runner group 的存取權限,並確認 runner 在線且閒置。忙碌的 runner 或 concurrency 佇列,與缺少監聽程序的修正方法並不相同。
- 前景正常、服務失敗:檢查擁有的 macOS 帳戶與工作階段、服務狀態、檔案系統權限、PATH 和服務日誌。重新安裝工具前,先測試準確的服務環境。
- 找不到 flutter 或 pod:在工作內檢查 command -v 和 PATH。為你的個人帳戶安裝工具,並不等於為專用的 runner 帳戶安裝。
- Xcode 報告 developer 目錄無效或缺少 SDK:檢查 xcode-select -p 和 xcodebuild -version,然後完成經批准的 Xcode 設定和平台安裝。
- 原生依賴報告架構不相容:檢查該依賴是否支援 Apple Silicon,以及是否有 Intel 版 Ruby 或二進位檔混入了工具鏈。在弄清楚哪個依賴需要 Rosetta 之前,不要全域套用。
- 程式碼簽署需要互動:分辨 keychain 已鎖、缺少憑證/私鑰、profile 不相符,以及缺少 entitlements。單有憑證檔案並不構成可用的簽署身分。
- 重新開機後 runner 消失:檢查磁碟解鎖、runner 帳戶的工作階段,以及已安裝的 LaunchAgent。昨天成功執行的安裝指令,並不是目前程序的證據。
- 建置停頓或耗盡磁碟:檢查運行中的程序和磁碟用量,設定工作逾時,並調查工作區的增長。避免終止無關的工作或刪除整個主目錄。
每次只改變一個原因,並重新執行同一個受信任的版本。把原本的錯誤和成功的驗證放在一起保存。這樣日後的失敗才容易診斷,亦可避免臨時的變通方法變成沒有記錄的機器依賴。
保護一部持久的建置機器
這個基本設定應使用只有受信任貢獻者的私人儲存庫。不受信任的 pull request 可以在機器上執行儲存庫腳本、依賴安裝 hook、測試和建置步驟。持久的 runner 可能在工作結束後仍保留惡意軟件或被修改的工具。不提供儲存庫 secret 有用,但並不能隔離檔案系統、本地網絡或主機上已有的憑證。
不要為這個 workflow 加入針對公開 fork 的 pull_request 觸發條件,亦絕不要把 pull_request_target 與在 Mac 上 checkout 並執行不受信任的 pull request 程式碼結合。手動觸發同樣不是安全沙盒:審查者必須信任 workflow 以及整個被 checkout 的版本,包括依賴和腳本。
- 日常工作中保持 runner 帳戶沒有特權,並移除無關的憑證和網絡存取。
- 預設使用唯讀的工作權限,不需要時停用持久化的 checkout 憑證,並鎖定經審查的第三方 action。
- 把簽署和發佈工作放在獨立受保護的環境和審查之後。服務支援時,優先使用短期憑證。
- 不受信任的建置應使用隔離、用完即棄的執行環境。工作完成後清理工作區,並不能證明一部已被入侵的持久主機是乾淨的。
實際成本與取捨
閒置的 Mac 並不是免費的基礎設施。請預算硬件折舊或更換、電費、寬頻、儲存裝置損耗、備份或復原容量,以及維護工具所花的時間。經濟上的問題是:就你的工作負載而言,控制權和重用是否值得這些成本。比較託管容量時,請查閱 GitHub 目前的價格和方案條款;本指南不承諾任何固定月費或節省金額。
作為示例計算,可在插座量度一個具代表性星期的平均功率。把瓦特換算為千瓦,乘以運行小時數,再乘以當地電費。加上每月的硬件攤分和工程時間成本。把總額與你實際會執行的託管工作比較,包括排隊延誤和失敗,而不是拿購買價與某個宣傳的每分鐘價格比較。
把停機時間納入決定。一部 Mac 就是單點故障。SSD 故障、網絡中斷或必需的作業系統重新開機,都可能阻礙所有 Apple 發佈。準備一個有文件記錄的替代方案,例如第二部已準備好的機器或託管 workflow,並在發佈期限前測試這個替代方案。
加入發佈自動化之前先建立信任
有用的里程碑是:在服務重新啟動後,能用已記錄的工具版本,從一個已知的 commit 重現驗證工作。當這個基線可靠之後,再加入專案專屬的整合測試、模擬器覆蓋、建置產物保留,以及另外受保護的簽署流程。每個新階段都應確立自己的結果,而不是沿用較早綠色檢查帶來的信心。
讓 runner 的職責保持精簡:執行受信任的程式碼、報告有用的失敗、並保持可復原。只要擁有者接受隨之而來的維護和安全工作,一部有良好文件記錄的 Mac mini 可以成為實用的 Flutter CI 機器。