接线目标:跑通一条可验收的 iOS 构建链路
在动手装 Runner 之前,我们先定义「接线完成」的验收标准,避免装完 Runner 却发现 Archive 卡在签名环节。本次实录的终点是:push 到指定分支 → GitHub Actions 在专属 M4 节点上触发 → checkout 代码 → 执行 xcodebuild archive 并成功产出 .xcarchive。TestFlight 上传不在本文范围,但 Archive 一旦稳定,后续 fastlane 或 altool 只是附加步骤。
测试仓库是一个中等规模的 SwiftUI 应用:约 60 个 Swift 源文件、CocoaPods 管理三方依赖、Release 配置走 Manual 签名。基准环境为 JexMac 新加坡节点上的 Mac mini M4(16 GB 统一内存、256 GB NVMe),Xcode 16.2 通过 xcodes 安装并锁定。
对照组是同一仓库在 runs-on: macos-14 托管 Runner 上的表现:UTC 13:00–18:00 时段(亚太下午)job 排队中位数 11 分钟,最长一次等待 19 分钟;且 macos-14 预装 Xcode 版本与本地开发机不一致,曾触发 Swift 6 语法兼容问题。自托管的价值在这里非常直观——等待时间从分钟级降到秒级,Xcode 版本完全由你写死在节点上。
托管 macOS Runner 的三类隐性成本
很多团队最初选 GitHub 托管 Runner 是因为「零运维」,但在 iOS 场景里,隐性成本往往比账单更棘手。
第一类是时间成本。GitHub 的 macOS 池容量有限,免费账户每月 2000 分钟额度按 macOS 10 倍权重折算,实际可用约 200 分钟。一个每天触发 8 次、单次构建 6 分钟的项目,月消耗约 1440 加权分钟——接近免费额度上限,稍微加个 nightly 构建就会超额。
第二类是环境漂移。macos-latest 标签会随 GitHub 基础设施升级而切换底层 Xcode,历史上多次出现「昨天绿、今天红」的情况。临时 workaround 是在 workflow 里加 sudo xcode-select 步骤,但每多一个版本切换就多 1–2 分钟开销,且无法保证与本地开发机完全一致。
第三类是调试成本。托管 Runner 每次 job 结束后环境被销毁,无法 SSH 进去复现问题。钥匙串弹窗、描述文件过期这类需要「登上去看一眼」的故障,在托管环境里只能反复 push 试日志——我们曾为一个 User interaction is not allowed 错误来回 push 了 7 次才定位到 partition list 缺失。
本文所有命令与耗时数据均在 JexMac 独享 Mac mini M4 物理节点上执行,节点位于新加坡数据中心,测试窗口为 2026 年 7 月下旬。硬件规格:Apple M4 · 10 核 CPU · 16 GB 统一内存 · 1 Gbps 独享带宽。
节点交付后:SSH 首连与环境基线
JexMac 控制台在付款确认后通常 1–5 分钟内推送 SSH 凭证。拿到公网 IP 后,建议先用 Ed25519 密钥接入并关闭密码登录,再开始装 Runner——Runner 进程会以当前 macOS 用户身份运行,SSH 安全基线要先打好。
-
01
写入 SSH 公钥
ssh-copy-id -i ~/.ssh/id_ed25519.pub jexmac@<节点IP>验证免密登录成功后,编辑
/etc/ssh/sshd_config设置PasswordAuthentication no,重启 sshd。 -
02
安装 Homebrew 与 xcodes
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"brew install xcodesorg/made/xcodesxcodes install 16.2 --experimental-fast-pass安装并锁定 Xcode 16.2(版本号按项目需求调整)。 -
03
验证构建链
xcodebuild -version应输出Xcode 16.2及 Build 号。xcodebuild -showsdks | grep iphoneos确认 iOS SDK 可用。 -
04
安装 CocoaPods(若项目使用)
sudo gem install cocoapods -n /usr/local/bin提前在节点上跑通一次
pod install,确认 Specs 仓库缓存就绪,避免 CI 首次构建卡在 repo update。
基线配置全部完成后,节点上应存在一个专用目录(本文用 ~/ci-runner)存放 Runner 与构建产物,与日常开发文件隔离。我们在该目录下预留 DerivedData 子目录,后续 workflow 里显式指定 -derivedDataPath,防止多项目并发时路径冲突。
GitHub 侧:Runner 令牌与标签路由设计
进入目标仓库 → Settings → Actions → Runners → New self-hosted runner,平台选 macOS ARM64。页面会生成一次性注册令牌(有效期 1 小时)和下载链接。
标签设计直接影响 workflow 能否路由到正确机器。我们的命名约定:
mac:所有 macOS 自托管节点的通用标签m4:标识 Apple Silicon M4 芯片,便于区分旧 Intel 节点xcode-16-2:锁定 Xcode 小版本,升级时改标签而非改 workflow 逻辑sg:数据中心区域(新加坡),多区域部署时可做就近路由
注册命令中的 --labels 参数一次性写入上述标签。workflow 里用数组形式匹配:
jobs:
ios-archive:
runs-on: [self-hosted, mac, m4, xcode-16-2]
concurrency:
group: ios-build-${{ github.ref }}
cancel-in-progress: true
concurrency 组确保同一分支不会并行跑两个 Archive——在 16 GB 内存的 M4 节点上,双项目并发 Clean Build 虽然能跑,但 DerivedData 争用会让单次耗时波动 30% 以上。如果团队有多条产品线,建议按产品线拆标签(如 product-a、product-b)并配多台节点,而不是在一台机器上硬并发。
自托管 Runner 在公开仓库中可被 fork PR 触发,恶意 workflow 将在你的 Mac 上执行任意代码。务必仅在私有仓库或 Organization 级别启用,且 Runner 进程运行在非管理员账户下。生产环境建议配合 GitHub Environment 保护规则,限制 secrets 仅在指定分支可用。
在 M4 节点安装 Runner 并配置 launchd 常驻
以下步骤在 SSH 会话中执行,Runner 版本号以 GitHub 注册页面显示为准(本文示例为 v2.321.0)。
-
01
下载并解压
mkdir -p ~/ci-runner/actions-runner && cd ~/ci-runner/actions-runnercurl -o actions-runner-osx-arm64-2.321.0.tar.gz -L \ https://github.com/actions/runner/releases/download/v2.321.0/actions-runner-osx-arm64-2.321.0.tar.gztar xzf ./actions-runner-osx-arm64-2.321.0.tar.gz -
02
交互式注册
./config.sh --url https://github.com/YOUR_ORG/YOUR_REPO \ --token YOUR_ONE_TIME_TOKEN \ --name jexmac-m4-sg-01 \ --labels mac,m4,xcode-16-2,sg \ --unattended--unattended跳过交互确认,适合脚本化部署。Work folder 保持默认_work即可。 -
03
安装 launchd 服务
./svc.sh install./svc.sh start验证:
./svc.sh status应显示active (running)。GitHub 仓库 Runners 页面出现绿色 Online 状态即注册成功。
launchd 会在系统重启后自动拉起 Runner,但有一个细节:Runner 以安装时的 macOS 用户身份运行,该用户必须已登录过一次(或启用自动登录),否则 launchd 可能无法访问 Keychain。我们在节点上创建了专用账户 ci-bot,首次通过 SSH 登录完成钥匙串初始化后再装 svc.sh,后续重启无需人工干预。
Runner 日志位于 ~/ci-runner/actions-runner/_diag/,每次 job 结束后会生成 Worker_*.log。排查「job 分配到了节点但没有步骤输出」类问题时,先看这里的 RunnerListener 日志,比 GitHub UI 上的 Annotations 信息更完整。
最小可用 Workflow:从 checkout 到 Archive
注册完成后,在仓库创建 .github/workflows/ios-archive.yml。下面是我们验证通过的最小版本,不含 TestFlight 上传,专注 Archive 产出:
name: iOS Archive
on:
push:
branches: [main, release/*]
pull_request:
branches: [main]
jobs:
archive:
runs-on: [self-hosted, mac, m4, xcode-16-2]
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Select Xcode
run: xcodes select 16.2
- name: Install pods
run: pod install --deployment
working-directory: ios
- name: Build archive
run: |
xcodebuild archive \
-workspace ios/MyApp.xcworkspace \
-scheme MyApp \
-sdk iphoneos \
-configuration Release \
-archivePath ./build/MyApp.xcarchive \
-derivedDataPath ~/ci-runner/DerivedData \
CODE_SIGN_STYLE=Manual \
CODE_SIGN_IDENTITY="Apple Distribution: Your Team (TEAMID)" \
PROVISIONING_PROFILE_SPECIFIER="MyApp AppStore"
env:
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
- name: Upload xcarchive artifact
uses: actions/upload-artifact@v4
with:
name: MyApp-xcarchive
path: ./build/MyApp.xcarchive
retention-days: 7
几个刻意保留的设计选择:
pod install --deployment 使用 Podfile.lock 锁定版本,避免 CI 与本地依赖不一致。-derivedDataPath 指向节点上的固定目录,多次构建可复用编译缓存,增量构建从 4 分钟降到约 1 分 40 秒。upload-artifact 把 xcarchive 上传到 GitHub,方便在没 SSH 权限的同事那里下载验证——artifact 保留 7 天,足够做一次 QA 抽检。
无界面环境:钥匙串与 Distribution 证书导入
Archive 步骤能否成功,取决于 CI 环境能否在无图形界面的情况下访问 Distribution 私钥。托管 Runner 预配置了系统钥匙串,但自托管节点需要你自己管理——这也是很多团队卡在「Runner 装好了但 Archive 报签名错误」的原因。
推荐做法:每次 job 创建临时钥匙串,导入 p12 后立即用于签名,job 结束自动销毁,避免私钥长期驻留磁盘。
# 在 "Build archive" 步骤之前加入
- name: Import signing certificate
run: |
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security set-keychain-settings -t 3600 -u build.keychain
echo "${{ secrets.CERTIFICATE_P12_BASE64 }}" | base64 --decode > cert.p12
security import cert.p12 \
-k build.keychain \
-P "${{ secrets.P12_PASSWORD }}" \
-T /usr/bin/codesign \
-T /usr/bin/xcodebuild
security set-key-partition-list \
-S apple-tool:,apple: \
-s -k "$KEYCHAIN_PASSWORD" build.keychain
rm -f cert.p12
env:
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
GitHub Repository Secrets 中需预先配置三个变量:KEYCHAIN_PASSWORD(临时钥匙串密码,随机字符串即可)、CERTIFICATE_P12_BASE64(Distribution 证书的 Base64 编码)、P12_PASSWORD(导出 p12 时设置的密码)。
| 报错关键词 | 常见根因 | 处理办法 |
|---|---|---|
User interaction is not allowed |
私钥 partition list 未设置,codesign 试图弹 UI | 补跑 set-key-partition-list 命令(见上方脚本) |
errSecItemNotFound |
证书导入了错误的钥匙串,或 default-keychain 未切换 | 确认 security default-keychain -s build.keychain 在 import 之前执行 |
| could not find signing certificate | CODE_SIGN_IDENTITY 字符串与钥匙串中 Identity 不完全匹配 |
运行 security find-identity -v -p codesigning build.keychain 复制完整名称 |
| Provisioning profile doesn't match | 描述文件过期或 Bundle ID / Capability 不一致 | 在 Apple Developer 后台重新生成,下载后通过 secrets 或 match 同步 |
导入成功后,可以用一条命令在 job 日志里快速验证:
security find-identity -v -p codesigning build.keychain | grep Distribution
看到 1 valid identities found 再进入 Archive 步骤,能省掉大量「签名失败但不知道卡在哪」的排查时间。
排错实录:三个让我们多 push 了一轮的问题
即使按上述步骤操作,首次接线仍可能遇到以下情况——都是我们真实踩过的坑,附上日志特征与解法。
标签不匹配:job 永远排队不执行
现象:GitHub Actions UI 显示 job 状态 Queued,Runners 页面节点却是 Online。根因是 workflow 里 runs-on 的标签与注册时不完全一致——比如 workflow 写了 xcode-16.2(点号),注册时用了 xcode-16-2(连字符)。GitHub 标签匹配是精确字符串比较,差一个字符就不会路由。
解法:在仓库 Runners 页面点击节点名称,查看实际标签列表,复制粘贴到 YAML 里,不要手打。
launchd 重启后 Runner Offline
现象:节点 reboot 或 JexMac 侧维护重启后,Runner 显示 Offline,需手动 SSH 执行 ./svc.sh start。根因是 launchd plist 里的 UserName 与 SSH 登录用户不一致,或该用户从未完成首次图形/session 登录。
解法:确认 ./svc.sh install 与 ./svc.sh start 在同一用户下执行;重启后检查 ./svc.sh status。若仍失败,查看 /Library/Logs/GitHubActionsRunner/ 下的 stderr 日志。
DerivedData 权限冲突
现象:第二次构建报 Unable to write to DerivedData 或某些 .o 文件 permission denied。根因是第一次 job 以 root 或不同用户生成了 DerivedData 目录,后续 job 无写权限。
解法:统一 DerivedData 路径并在 workflow 开头加清理步骤:rm -rf ~/ci-runner/DerivedData && mkdir -p ~/ci-runner/DerivedData。或者在每次 Archive 前只清理对应项目的 hash 子目录,保留可复用的编译缓存。
没有常驻 Mac 时:按构建节奏租用专属节点
走到这里,你可能已经意识到:GitHub Actions 自托管 Runner 的技术门槛并不高,真正的瓶颈是有没有一台 24 小时在线、Xcode 版本可控的 macOS 物理机。本地 MacBook 不适合做 CI 主机——8 GB 内存机型跑 Archive 时风扇拉满还频繁 swap;公司采购 Mac mini 则要面对固定资产审批、机房托管和证书轮换时的现场维护。
我们的做法是:在迭代冲刺期(例如发版前两周)租用 JexMac 独享 Mac mini M4 节点充当 Runner 主机,上线稳定后按周续租或释放。标准配置 16 GB 统一内存、256 GB NVMe、1 Gbps 独享带宽,按天 $21.5 起,付款后 1–5 分钟交付 SSH / VNC 接入,无合同锁定。五处节点——新加坡、日本(东京)、韩国(首尔)、中国香港、美国东部——可按团队地理位置选择,降低 git fetch 与 CocoaPods Specs 同步延迟。
与 GitHub 托管 macOS Runner 对比:高峰排队 10–20 分钟 vs 自托管 25 秒内开工;macOS 分钟按 10 倍权重计费 vs 固定日租成本可预测。与自购硬件对比:零 upfront 投入,版本升级时换标签或重装 Xcode 即可,不必等采购周期。
同一台节点还可以兼做远程开发桌面——浏览器 VNC 连上去调试 UI、Instruments 抓性能,CI 空闲窗口不浪费。对于两三人的小团队,「一台 M4 物理机 = Runner + 远程 Mac 开发环境」往往比分开买托管 Runner 分钟和本地升级内存更划算。
把 Runner 接到你的 M4 节点上
文中全部命令已在 JexMac 独享 Mac mini M4 物理节点验证。开通 → SSH 接入 → 按步骤注册 Runner,最快一小时内跑通 Archive。按天起租,冲刺结束可释放,无年约捆绑。