开发
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 脚本、截图或 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 提交到默认分支。打开 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 机器。