GitHub Actions Mac Runner 一直排队时,本周不要先增加 Mac 节点:先依次确认工作流等待条件、标签与 Runner Group 路由、节点是否真实空闲,以及 macOS Runner 服务能否领取任务;只有路由修正或服务恢复后仍持续积压,才进入增加远程 Mac 容量或拆分任务池的决策。
这篇文章适合使用 GitHub Actions 自托管 Mac 执行 Xcode 构建,却发现 Job 一直排队的移动开发者;也适合负责标签、Runner Group、仓库访问权限和常驻服务的 DevOps 工程师,以及需要判断节点应修复、重新注册还是扩容的研发平台负责人。
先锁定排队层级
“Runner 管理页显示 Idle”并不等于目标 Job 已经具备领取条件。GitHub Actions 可能仍在等待前置 Job、环境审批、并发组名额,或者等待满足 runs-on 条件的自托管 Runner。
排查时先打开工作流运行记录,进入具体 Job,记录以下证据:
- Job 当前显示的是
queued、pending还是等待审批; - Job 是否已经通过
needs、分支、环境和手动审批等前置条件; - 工作流实际解析出的
runs-on值; - Runner 管理页中的在线状态、忙闲状态、标签和所属组;
- 是否配置了
concurrency,导致同一并发组中只有一个任务可以运行。
GitHub Actions 并发控制文档说明,并发组会限制同组 Job 的同时执行;默认情况下,同一个并发组通常只保留一个等待中的任务,新增任务可能替换已有等待任务。若把这类等待误认为 Mac 节点不足,扩容并不能解决根因。
建议在修改 YAML、标签或权限前,把当前状态保存下来:
mkdir -p runner-investigation
date -u > runner-investigation/checked-at.txt
cp .github/workflows/ios.yml runner-investigation/ios.yml.snapshot
同时记录 Job 链接、运行编号、目标 runs-on、Runner 名称、Runner Group 和管理页截图。这样可以避免边修改边排查,最后无法判断到底是哪一项变化恢复了路由。
⚠️ 如果 Job 还没有通过环境审批或 needs 前置条件,不要重启 Runner。此时 Runner 根本没有机会领取任务,重启只会制造新的无关变量。
runs-on 与标签路由
组合标签必须全部满足
自托管 Runner 的 runs-on 不是“满足其中一个标签即可”。当工作流写成数组时,Runner 必须同时拥有全部标签;GitHub 官方也建议使用包含 self-hosted、操作系统和架构信息的标签组合。选择 Job 执行 Runner 的官方说明给出了这一匹配规则。
例如:
jobs:
build:
runs-on: [self-hosted, macOS, arm64, xcode-ci]
这段配置要求目标节点同时符合:
self-hosted;macOS;arm64;xcode-ci。
如果节点重建后只保留了默认标签,或者自定义标签从 xcode-ci 改成了 ios-build,Runner 管理页仍可能显示在线和 Idle,但 Job 不会被路由到该节点。标签大小写不应作为唯一排查依据,因为 GitHub 文档说明自定义标签不区分大小写;真正需要核对的是拼写、是否仍然存在,以及标签是否被重新注册后的节点继承。自托管 Runner 标签文档
用最小诊断 Job 验证路由
不要直接把生产 Xcode Job 改成宽泛的 self-hosted。这样虽然可能让任务运行,却会绕过架构、工具链和隔离策略,导致错误节点执行签名或发布任务。
可以先建立一个无敏感信息的诊断 Job:
name: runner-route-check
on:
workflow_dispatch:
jobs:
route:
runs-on: [self-hosted, macOS, arm64, xcode-ci]
steps:
- name: Print runner identity
run: |
echo "runner=$RUNNER_NAME"
echo "os=$RUNNER_OS"
echo "arch=$(uname -m)"
sw_vers
预期结果是 Job 能够开始执行,并输出目标 Runner 名称、macOS 系统信息和硬件架构。如果这个最小 Job 也持续排队,优先处理标签或组权限;如果它能运行而生产 Job 仍排队,再检查生产工作流中的动态表达式、环境审批、并发配置和额外标签。
Runner Group 与仓库访问
标签匹配并不代表仓库有权使用该节点。Runner Group 是另一层访问边界,尤其是在组织级或企业级 Runner 中,节点可能属于正确的组,但目标仓库没有被加入该组的允许范围。
工作流可以单独指定组:
jobs:
build:
runs-on:
group: macos-builders
也可以同时指定组和标签:
jobs:
build:
runs-on:
group: macos-builders
labels: [self-hosted, macOS, arm64, xcode-ci]
后一种写法要求 Runner 同时满足组成员资格和全部标签条件。使用自托管 Runner 的工作流文档明确指出,组与标签组合时,节点必须同时满足两类条件。
排查 Runner Group 时,按以下顺序核对:
- Runner 是否位于预期的组织或企业层级;
- Runner 是否被放入正确的 Runner Group,而不是仍留在默认组;
- 目标仓库是否被该组允许访问;
- 组是否限制了可访问的仓库或工作流;
- 工作流中的组名是否写错,或者引用了不同层级的同名组。
GitHub 官方 Runner Group 管理文档说明,组织管理员可以把组设置为允许全部仓库或仅允许选定仓库;如果选择了限定仓库模式,目标仓库未被加入时,标签再正确也无法完成路由。
可以临时运行一个不包含源码、密钥和签名操作的诊断工作流,用于验证组访问。不要为了测试而直接放宽整个组织的组权限,更不要把生产仓库临时改为公开可访问。
⚠️ 撤销 Runner 注册、删除 .runner 文件或放宽 Runner Group 权限都会改变恢复路径。删除 Runner 会移除 GitHub 侧注册信息,并可能清除本机服务配置;执行前应先保存标签、组归属、工作目录和服务文件。官方移除 Runner 说明
Idle 状态与真实占用
如果标签和组权限都正确,下一步才是确认“符合条件的 Runner 是否真的空闲”。GitHub 的路由逻辑会寻找在线且 Idle、同时匹配 runs-on 标签和组的 Runner;如果找不到,Job 会继续排队。自托管 Runner 路由参考
在 Mac 节点上,以下任务最容易造成表面空闲、实际不可用:
- Xcode 编译已经退出,但
xcodebuild、swiftc或脚本子进程仍在运行; - Simulator 测试卡在启动、关机或测试清理阶段;
- 签名、归档或发布脚本等待钥匙串、网络或人工输入;
- 工作区被上一次任务锁定,新的任务无法安全复用;
- Runner 主进程恢复在线,但任务执行器仍停在异常状态。
SSH 登录节点后,先做只读检查:
ps aux | egrep 'Runner.Listener|Runner.Worker|xcodebuild|simctl|fastlane' | grep -v grep
pgrep -fl 'Runner.Listener|Runner.Worker|xcodebuild|simctl'
不要看到进程就直接 kill -9。先根据 GitHub Job 日志确认对应任务是否已经结束,再清理明确失控的子进程,并保留进程列表和时间戳。若多个任务确实在正常运行,说明这是容量或任务分池问题;若没有活动任务却长期无法回到可领取状态,则应转入服务层排查。
macOS 服务与领取链路
SSH 能登录,只能证明远程主机网络可达,不能证明 GitHub Actions Runner 服务正常。Mac Runner 还需要持续运行 Runner 应用、保持网络连接,并使用有权限访问工作目录和工具链的服务账户。
在 Runner 安装目录执行:
cd ~/actions-runner
./svc.sh status
官方 macOS 排障文档建议使用 launchctl 检查以服务方式运行的 Runner,并从 _diag 目录读取 Runner 与 Worker 日志。macOS 自托管 Runner 监控文档
ls -lt _diag | head
tail -n 120 _diag/Runner_*.log
tail -n 120 _diag/Worker_*.log
cat .service
排查重点包括:
Runner_日志是否显示持续断线、认证失败或连接恢复;Worker_日志中是否出现任务已分配但没有完成领取;.service文件指向的plist是否仍存在;- launchd 服务使用的用户是否能够访问工作目录;
- 重启后服务是否自动加载,而不是只在 SSH 会话中运行;
- 服务账户的钥匙串、Xcode 路径和环境变量是否与交互式登录不同。
可以先重启 Runner 服务,而不是立即删除注册信息:
./svc.sh stop
./svc.sh start
./svc.sh status
如果日志明确显示注册状态损坏,再考虑重新注册。重新注册前保存当前 .service、标签、Runner Group 和工作流目标;重新注册后,官方文档提示替换现有 Runner 时需要重新分配自定义标签。官方标签配置说明
修复、重建与扩容判定
排查不能以“Job 最终跑起来了”作为唯一结论。至少应执行三类复测:
- 最小命令任务:验证标签、组权限和基本领取链路;
- 真实 Xcode 构建:验证工作目录、Xcode、签名和产物回传;
- 生产近似任务:使用实际目标标签、相同环境变量和相近的构建脚本,确认不是诊断 Job 的特殊配置造成假成功。
复测时记录 Job 创建、进入执行、Runner 领取和产物回传四个节点。GitHub 官方路由参考还说明,任务分配后若 Runner 在 60 秒内没有领取,任务会重新进入队列;如果一直找不到合格的在线空闲 Runner,Job 最长排队超过 24 小时会失败。自托管 Runner 路由规则
可勾选验收清单
- [ ] 已确认 Job 不是在等待
needs、环境审批或并发组; - [ ] 已保存当前 YAML、Job 链接、运行编号和
runs-on; - [ ] 已逐项核对
self-hosted、系统、架构和工具链标签; - [ ] 已确认 Runner Group 与目标仓库访问策略;
- [ ] 已用最小诊断 Job 验证目标路由;
- [ ] 已检查活动 Job、
xcodebuild、Simulator 和 Runner 子进程; - [ ] 已查看
_diag中的 Runner 与 Worker 日志; - [ ] 已确认
launchd服务、工作目录和服务账户权限; - [ ] 已优先尝试服务恢复,而不是直接删除注册;
- [ ] 已完成最小任务、真实构建和生产近似任务三轮复测;
- [ ] 已记录修复前后的节点状态、任务领取和产物回传证据。
下表用于区分故障类型,不把所有排队现象都归因于机器数量:
| 观察结果 | 更可能的原因 | 处置建议 |
|---|---|---|
| 最小 Job 也一直 queued | 标签、组权限或路由条件不满足 | 修正 runs-on、标签或仓库访问策略 |
| Runner 在线但日志没有领取记录 | 服务、网络或注册链路异常 | 检查 _diag、launchd 和服务账户 |
| 最小 Job 成功,真实构建失败 | Xcode、签名、工作区或脚本问题 | 保留 Runner,单独修复构建环境 |
| 所有条件正确且节点都有真实活动任务 | 任务池确实饱和 | 拆分构建、测试、发布任务或增加容量 |
| 服务恢复后仍反复掉线 | 节点稳定性或托管环境问题 | 重建隔离节点并重新验收 |
| 决策 | 适用证据 | 不应采取的动作 |
|---|---|---|
| 修复现有节点 | 标签、权限或服务问题,硬件仍可稳定执行 | 不要先扩容掩盖路由故障 |
| 重建隔离节点 | 注册状态损坏、服务文件混乱、环境不可审计 | 不要在没有保存恢复入口时删除旧节点 |
| 增加远程 Mac 容量 | 多轮复测证明节点都在正常工作,队列仍稳定积压 | 不要仅因管理页显示 Idle 就购买更多节点 |
| 拆分任务池 | 构建、Simulator、签名发布互相独占 | 不要让所有任务长期共享一个宽泛标签 |
当前节点与远程 Mac 方案
如果当前方案是把一台开发者本地 Mac 长期充当 CI 节点,常见问题是本地使用与构建任务争抢资源、机器休眠或网络变化导致 Runner 失联,以及重启后 launchd、钥匙串和工作目录状态不一致。若改用临时云端 Linux,也无法直接替代依赖 Xcode、macOS SDK、Simulator 或 Apple Silicon 工具链的任务。
在完成标签、组权限和服务排查后,可以先用一台隔离的远程 Mac 复现同一份工作流,验证路由和构建环境是否稳定。若需要按周、按月或按季度获得独立 Mac 节点,可进一步查看 NodeMini 的远程 Mac 方案;如果团队正在比较 Mac mini 作为持续构建节点的部署方式,也可以参考 Mac mini 云算力订购方案。
这种方式的价值不在于“所有排队都靠增加机器解决”,而在于把故障节点、生产节点和备用构建节点隔离开:前者用于修复和取证,后者用于稳定执行工作流,备用节点则在现有 Mac 无法稳定领取任务或需要临时构建能力时提供回退路径。