GitHub Actions Mac Runner 一直排队时,本周不要先增加 Mac 节点:先依次确认工作流等待条件、标签与 Runner Group 路由、节点是否真实空闲,以及 macOS Runner 服务能否领取任务;只有路由修正或服务恢复后仍持续积压,才进入增加远程 Mac 容量或拆分任务池的决策。

这篇文章适合使用 GitHub Actions 自托管 Mac 执行 Xcode 构建,却发现 Job 一直排队的移动开发者;也适合负责标签、Runner Group、仓库访问权限和常驻服务的 DevOps 工程师,以及需要判断节点应修复、重新注册还是扩容的研发平台负责人。

01

先锁定排队层级

“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 根本没有机会领取任务,重启只会制造新的无关变量。

02

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 仍排队,再检查生产工作流中的动态表达式、环境审批、并发配置和额外标签。

03

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 时,按以下顺序核对:

  1. Runner 是否位于预期的组织或企业层级;
  2. Runner 是否被放入正确的 Runner Group,而不是仍留在默认组;
  3. 目标仓库是否被该组允许访问;
  4. 组是否限制了可访问的仓库或工作流;
  5. 工作流中的组名是否写错,或者引用了不同层级的同名组。

GitHub 官方 Runner Group 管理文档说明,组织管理员可以把组设置为允许全部仓库或仅允许选定仓库;如果选择了限定仓库模式,目标仓库未被加入时,标签再正确也无法完成路由。

可以临时运行一个不包含源码、密钥和签名操作的诊断工作流,用于验证组访问。不要为了测试而直接放宽整个组织的组权限,更不要把生产仓库临时改为公开可访问。

⚠️ 撤销 Runner 注册、删除 .runner 文件或放宽 Runner Group 权限都会改变恢复路径。删除 Runner 会移除 GitHub 侧注册信息,并可能清除本机服务配置;执行前应先保存标签、组归属、工作目录和服务文件。官方移除 Runner 说明

04

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 日志确认对应任务是否已经结束,再清理明确失控的子进程,并保留进程列表和时间戳。若多个任务确实在正常运行,说明这是容量或任务分池问题;若没有活动任务却长期无法回到可领取状态,则应转入服务层排查。

05

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 时需要重新分配自定义标签。官方标签配置说明

06

修复、重建与扩容判定

排查不能以“Job 最终跑起来了”作为唯一结论。至少应执行三类复测:

  1. 最小命令任务:验证标签、组权限和基本领取链路;
  2. 真实 Xcode 构建:验证工作目录、Xcode、签名和产物回传;
  3. 生产近似任务:使用实际目标标签、相同环境变量和相近的构建脚本,确认不是诊断 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、签名发布互相独占 不要让所有任务长期共享一个宽泛标签
07

当前节点与远程 Mac 方案

如果当前方案是把一台开发者本地 Mac 长期充当 CI 节点,常见问题是本地使用与构建任务争抢资源、机器休眠或网络变化导致 Runner 失联,以及重启后 launchd、钥匙串和工作目录状态不一致。若改用临时云端 Linux,也无法直接替代依赖 Xcode、macOS SDK、Simulator 或 Apple Silicon 工具链的任务。

在完成标签、组权限和服务排查后,可以先用一台隔离的远程 Mac 复现同一份工作流,验证路由和构建环境是否稳定。若需要按周、按月或按季度获得独立 Mac 节点,可进一步查看 NodeMini 的远程 Mac 方案;如果团队正在比较 Mac mini 作为持续构建节点的部署方式,也可以参考 Mac mini 云算力订购方案。

这种方式的价值不在于“所有排队都靠增加机器解决”,而在于把故障节点、生产节点和备用构建节点隔离开:前者用于修复和取证,后者用于稳定执行工作流,备用节点则在现有 Mac 无法稳定领取任务或需要临时构建能力时提供回退路径。