你的起点
从 KVMFLUX 租用一台专属云端 Mac mini M4,你会拿到真实 Apple 硬件上等同 root 的管理权限:16GB 统一内存、256GB SSD、预装 macOS,可通过 SSH 与 VNC 访问。没有任何共享,也没有虚拟化,所以 xcodebuild 看到的是完整的 M4,Secure Enclave 的行为也和你桌上那台一样。
选一个离你的产物存储最近的区域,而不是离团队最近。一个 runner 与 Git 托管和 CDN 通信的频率,远高于任何人类真正操作它的频率——覆盖新加坡、日本、韩国、香港、美国东部与美国西部。
第一步 — 先把 SSH 锁死,再做别的
机器出厂时开启了密码登录,方便你第一次进去。第一次会话就该结束这种状态:先推密钥,再关密码。
ssh-keygen -t ed25519 -f ~/.ssh/kvmflux_ci -C "ci-runner"
ssh-copy-id -i ~/.ssh/kvmflux_ci.pub admin@<YOUR_MAC_HOST>
sudo tee -a /etc/ssh/sshd_config <<'EOF'
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
EOF
sudo launchctl kickstart -k system/com.openssh.sshd
对 CI 节点来说还有两项设置很关键:阻止机器休眠,以及在没有接显示器时不要让后台守护进程暂停。
sudo pmset -a sleep 0 displaysleep 0 disksleep 0
sudo systemsetup -setrestartfreeze on
保留 VNC 作为救命通道。有些 Xcode 弹窗——许可协议提示、模拟器首次运行权限——只会在图形界面里出现,你需要一条在 SSH 配置出问题时也能进去的路。
第二步 — 安装 Xcode 工具链
跳过 App Store。xcodes 可以非交互式地安装指定的 Xcode 版本,正是无头节点需要的。先装 Homebrew,再装工具链。
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
brew install xcodesorg/made/xcodes aria2
xcodes install 16.2 --experimental-unxip
sudo xcodes select 16.2
sudo xcodebuild -license accept
sudo xcodebuild -runFirstLaunch
确认你实际要构建的目标平台。如果流水线要跑模拟器测试,现在就下载运行时,别等第一个任务时再下。
xcodebuild -downloadPlatform iOS
xcrun simctl list runtimes
xcodebuild -version
加上移动端流水线常用的工具:fastlane 用于 lane 与签名,如果仓库带二进制资产就装 git-lfs。
brew install fastlane git-lfs jq
git lfs install --system
第三步 — 把机器注册为 runner
GitHub Actions、GitLab 和 Buildkite 的模式都一样:下载一个代理,给它一个限定范围的 token,把它当作服务运行。这是 Apple Silicon 上 GitHub Actions 版本的做法。
mkdir ~/actions-runner && cd ~/actions-runner
curl -o runner.tar.gz -L \
https://github.com/actions/runner/releases/download/v2.321.0/actions-runner-osx-arm64-2.321.0.tar.gz
tar xzf runner.tar.gz
./config.sh --url https://github.com/your-org/your-app \
--token <REGISTRATION_TOKEN> \
--labels macos,arm64,m4 --unattended
./svc.sh install && ./svc.sh start
用 svc.sh 安装会把 runner 接入 launchd,这样重启后即使没有登录的桌面会话也能继续存活。用标签来指定目标:
jobs:
build:
runs-on: [self-hosted, macos, m4]
steps:
- uses: actions/checkout@v4
- run: xcodebuild -scheme App -destination \
'platform=iOS Simulator,name=iPhone 16' test
注册 token 一小时后失效,且只能用一次——这部分没问题。runner 的长期凭证保存在 runner 目录里的 .credentials 文件中,所以让这个用户账户保持"无趣":不放个人 SSH 密钥,不留浏览器会话。
第四步 — 给 CI 一个它能自己解锁的密钥串
签名是大多数自托管 Mac 配置最容易卡住的地方。解决方案是一个专用密钥串,由你的 lane 创建、解锁、用完即扔——这样登录密钥串永远不会弹窗,也不会在任务之间泄漏。
KEYCHAIN=ci.keychain-db
security create-keychain -p "$KEYCHAIN_PASS" $KEYCHAIN
security set-keychain-settings -lut 3600 $KEYCHAIN
security unlock-keychain -p "$KEYCHAIN_PASS" $KEYCHAIN
security import dist.p12 -k $KEYCHAIN -P "$P12_PASS" \
-T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple: \
-s -k "$KEYCHAIN_PASS" $KEYCHAIN
security list-keychains -d user -s $KEYCHAIN login.keychain-db
set-key-partition-list 这一行是所有人都容易忘掉的——没有它,codesign 会一直挂着等一个永远不会出现的图形密码弹窗。如果你用 fastlane,配合一个私有证书仓库的 match 能把这整段全部自动化。
缓存:把成果留在磁盘上
专属机器胜过临时 runner 的核心原因正是状态可以持久保留。善用这一点,别每个任务都重新下载全世界:
- DerivedData — 用
-derivedDataPath ~/ci-cache/dd指向一个固定路径,增量构建时间通常能从几分钟降到几秒。 - Swift 包 — 设置
-clonedSourcePackagesDirPath ~/ci-cache/spm,让依赖解析在任务之间复用已有的检出内容。 - CocoaPods 与 Homebrew — 两者默认都缓存在 runner 用户的主目录下;只是别在构建之间清空工作区。
诚实估算磁盘用量。256GB SSD 上,Xcode 加运行时大约要占用 40GB,一个活跃项目的缓存会逐渐涨到 60–80GB。按计划清理,而不是等出问题才清理:
find ~/ci-cache/dd -maxdepth 1 -mtime +14 -exec rm -rf {} +
xcrun simctl delete unavailable
df -h /
如果项目自带大体量资产,或需要并存多个 Xcode 版本,额外 SSD +1TB 选配只要 $11.7/月,比到发布时排查磁盘写满的问题划算得多。
并发:一台 M4 能扛几个任务?
一台 16GB 内存的 M4 处理一个重任务表现很好:一次完整的干净构建加模拟器测试套件能用满所有核心。同时跑两个基于模拟器的任务对小应用还行,但内存压力上来后两个都会掉速。
我们经过几个月流水线值守后总结的经验规则:
- 对构建加测试类工作负载,一个 runner 进程一次跑一个任务,是最稳妥的默认设置。
- 只有在加了第二台机器之后,才把 lint、单元测试和 UI 测试拆成独立任务——放在同一台机器上排队只会带来额外开销。
- 夜间任务是白得的算力:把依赖审计和截图跑批安排在该区域时区的非工作时段。
当排队时间成为瓶颈时,加一台租用节点能线性扩容——用同样的标签注册即可,调度器会自动做负载均衡。按日还是按月节点的算账是另一个话题,我们在租期成本拆解里算过;相关工作流也可以在应用场景概览里看到。
整套配置,浓缩版
- 仅密钥登录 SSH,关闭睡眠,保留 VNC 作为后备。
- 用
xcodes安装 Xcode,接受许可,预先下载运行时。 - 把 runner 注册为带有意义标签的
launchd服务。 - 专用 CI 密钥串,设置好分区列表。
- 持久化缓存并配合定时清理。
整套操作动手时间不到一个小时,结果是一个团队不再需要操心的构建节点——这正是重点所在。
专属硬件,美元计费,随时可取消。