同一份 YAML 没有改动,构建却突然在 Xcode、原生依赖或签名步骤失败。

最快解法:不要继续把 macos-latest 当作固定环境,先锁定明确的 macOS 标签,再用并行任务验证 Xcode、运行时、架构、签名与缓存;发布流程若依赖持久环境,则保留独立远程 Mac 自托管 Runner。

这篇文章适合仍在工作流中使用 macos-latest、尚未核对实际 Runner 镜像的移动端开发者;也适合维护 CocoaPods、SwiftPM、Node.js、Ruby 或原生二进制依赖的 CI 工程师。需要决定继续使用托管 Runner,还是增加长期在线 Mac 构建节点的平台负责人,也可以直接按文中的条件分支执行。

最后更新于 2026 年 8 月 23 日。本文的迁移日期、Runner 标签、架构和工具链信息核实自官方迁移公告、托管 Runner 文档、Runner 镜像清单与 Apple 开发者资料;发布前仍应运行最小探测任务确认你所在账户看到的实际环境。

GitHub Actions macOS 26 迁移的判断基线

截至 2026 年 8 月 23 日,macOS 26 Runner 已进入正式可用阶段。官方曾公告 macos-latest 将在 2026 年 6 月 15 日开始、用约 30 天完成从 macOS 15 向 macOS 26 的迁移;如果你的工作流在这段时间或之后出现失败,不能只看提交记录判断原因。应先查看任务日志中的操作系统、架构、镜像版本和 Xcode 路径。(GitHub Changelog 的迁移公告)

这里有一个容易误判的地方:macos-latest 是移动标签,不是永久版本号。官方 Runner 文档说明,-latest 代表当前提供的最新稳定镜像,但不一定等于系统厂商刚发布的最新系统;不同 Runner 类型还可能拥有不同的架构和标签映射。(GitHub 托管 Runner 标签说明)

在失败任务最前面加入探测步骤:

- name: Inspect runner
  run: |
    set -euxo pipefail
    sw_vers
    uname -a
    uname -m
    xcode-select -p
    xcodebuild -version
    clang --version
    node --version || true
    ruby --version || true
    python3 --version || true
    brew --version || true
    system_profiler SPHardwareDataType

你需要保存这几项证据:

  • sw_vers:实际 macOS 版本,而不是 YAML 中写了什么。
  • uname -m:确认当前是 arm64 还是 x86_64。
  • xcode-select -p 与 xcodebuild -version:确认命令行工具指向和默认 Xcode。
  • Runner 日志中的镜像版本:用于和官方 macOS Runner 镜像清单逐项比对。
  • 依赖安装和签名步骤的完整日志:不要只保留最后一行错误。

把流水线先分成三类,迁移优先级会更清楚:

  1. 无状态测试任务:每次都重新拉取依赖、编译和测试,通常可以先并行验证 macOS 26。
  2. Apple 平台构建任务:涉及 Xcode、模拟器、归档或原生扩展,必须先验证工具链与架构。
  3. 发布签名任务:涉及证书、Provisioning Profile、钥匙串和归档产物,不应直接把生产 Job 改成 macos-latest。

macos-latest 的实际系统如何确认

不要用搜索结果、旧文章或标签名称推断答案。官方文档列出了 macos-latest、macos-15、macos-26 等标签,同时说明不同 Runner 类型对应不同架构与规格;具体任务仍应以你的任务日志和当天镜像清单为准。

建议在迁移分支中暂时同时运行:

jobs:
  verify-macos:
    strategy:
      fail-fast: false
      matrix:
        runner: [macos-15, macos-26]
    runs-on: ${{ matrix.runner }}

如果你还要验证 Intel 与 Apple Silicon,则不要把系统版本和架构混为一个变量:

strategy:
  matrix:
    include:
      - runner: macos-26
        expected_arch: arm64
      - runner: macos-26-intel
        expected_arch: x86_64

标签是否可用、是否属于公开预览,以及标签对应的架构,都应对照官方 Runner 文档和当前macOS 26 镜像 README检查。不要因为一次构建成功,就把它写成长期兼容承诺。

Xcode、SDK 与模拟器的迁移检查

macOS 26 迁移后,最先暴露的往往不是 Swift 代码,而是“默认 Xcode 变了”。当前镜像清单会记录系统版本、镜像版本、预装 Xcode 和运行时;清单中还可能标明默认 Xcode 的切换日期,因此“机器上安装了某个版本”不等于 xcodebuild 默认使用它。

构建失败后的核验证据

先记录升级前后以下内容:

  • xcodebuild -version 的完整输出。
  • /Applications 下实际存在的 Xcode 路径。
  • xcode-select -p 的选择结果。
  • xcodebuild -showsdks 的 SDK 列表。
  • 测试目标使用的模拟器名称、系统版本和架构。
  • xcodebuild -showBuildSettings 中的 SDKROOT、ARCHS 和部署目标。

Apple 的Xcode 系统要求文档显示,不同 Xcode 版本对应不同的 macOS、SDK 和部署目标;例如 Xcode 26 面向 iOS 26 等 SDK,而 Xcode 27 beta 属于测试阶段工具链,不能与稳定版发布流程混写。

迁移动作与通过标准

第一步,显式选择工具链,而不是依赖镜像默认值:

- name: Select Xcode
  run: |
    sudo xcode-select -s "/Applications/Xcode_26.0.1.app"
    xcode-select -p
    xcodebuild -version

路径必须根据当天镜像清单调整,不能照抄示例。第二步,为测试、构建和发布建立版本矩阵,例如稳定 Xcode 与候选 Xcode 并行跑,但只允许稳定版本进入生产签名 Job。

第三步,保留旧系统标签的回退任务。回退任务不是让所有工作流永久停留在旧环境,而是在新镜像失败时快速确认问题是否来自系统或工具链。

通过标准至少包括:

  • 同一提交在目标标签上连续构建成功。
  • 单元测试、UI 测试和关键模拟器目标均完成。
  • 归档产物的 Bundle ID、版本号和签名状态符合预期。
  • 发布 Job 可以在不修改证书和密钥的情况下回退。
  • Xcode 版本、SDK 版本和镜像版本已写入构建记录。

Apple Silicon 与 Intel 依赖边界

“能编译”不等于“在两个架构上都能运行”。Apple Silicon 环境下,失败点通常集中在预编译二进制、原生扩展、脚本中的架构判断,以及由一个架构生成、再被另一个架构消费的缓存或产物。

官方文档列出了 macOS arm64 与 Intel Runner 的不同标签,也提醒社区 Action 可能需要额外处理;arm64 macOS Runner 还不支持嵌套虚拟化,并且没有静态 UUID。(GitHub 自托管 Runner 与架构说明)

四类依赖逐项排查

预编译二进制:对关键 .framework、.a、命令行工具执行:

file path/to/binary
lipo -info path/to/binary 2>/dev/null || true

如果只显示 x86_64,它不能直接证明 Apple Silicon 原生兼容;如果依赖通过 Rosetta 运行,也不能把结果当作原生 arm64 验收。

原生扩展:重点检查 Ruby Gem、Node 原生模块、Python 扩展、C/C++ 插件和 Swift Package 中的编译步骤。失败日志中如果出现架构不匹配、链接器找不到库或加载动态库失败,应先确认实际执行架构,再决定升级依赖还是使用兼容构建参数。

脚本架构判断:搜索 uname -m、arch、x86_64、arm64、brew --prefix 等硬编码。脚本若把 Homebrew 路径、缓存目录或下载地址写死,系统升级后可能表现为“找不到命令”,实际原因却是路径和架构变了。

跨架构产物:不要把 arm64 构建产物缓存后直接给 Intel Job 使用,反之亦然。缓存键至少应包含系统、架构、Xcode 和锁文件摘要,例如:

key: ${{ runner.os }}-${{ runner.arch }}-xcode26-${{ hashFiles('**/Package.resolved', '**/Podfile.lock') }}

只有在真实目标架构上完成测试,才能证明 Apple Silicon 与 Intel 均可运行。

运行时、包管理器与缓存状态

macOS 26 迁移带来的脚本错误,经常被笼统归因于操作系统。实际上,Node.js、Ruby、Python、Homebrew、Clang、OpenSSL 或某个 Action 版本的变化,都可能独立造成失败。当前镜像 README 会列出预装运行时版本,这些值会随镜像更新,不能作为永久常量写进文章或脚本。

先输出版本,再选择修复方向

建议把环境探测放在依赖安装之前,并记录可执行文件路径:

command -v node ruby python3 clang openssl brew || true
node --version || true
ruby --version || true
python3 --version || true
clang --version || true
openssl version || true
brew config || true

然后按最小复现区分原因:

  • 只有换 Runner 标签才失败:优先怀疑系统镜像或默认工具链。
  • 同一标签上,升级某个 Action 后才失败:锁定 Action 版本并单独回归。
  • 依赖锁文件不变仍失败:检查预编译包、编译器和系统库。
  • 清空缓存后失败、恢复旧缓存后成功:检查缓存是否混入了旧架构或旧 SDK 产物。
  • 仅发布 Job 失败:优先检查钥匙串、证书、Profile 和权限,不要先改业务代码。

缓存键不能只写成 macos-build-${{ hashFiles(...) }}。系统和架构变化后,旧缓存可能仍然命中。官方缓存文档说明,精确键命中后才算 cache hit;缓存键最长为 512 个字符,且已有缓存不能原地修改,只能创建新键。(GitHub Actions 依赖缓存文档)

注意:缓存不是持久化工作目录。它适合复用依赖下载和可验证的中间结果,不适合保存签名身份、登录凭据或任何秘密信息。具有读取权限的拉取请求可能访问缓存内容。

迁移验收与评分标准

你可以把迁移结果按下面的独立评分判断,而不是只看“主分支是否绿灯”:

  • 系统与标签,2 分:日志记录了实际 macOS、镜像版本和 runs-on 标签。
  • 架构兼容,2 分:目标架构完成了依赖安装、编译和运行测试。
  • Xcode 与 SDK,2 分:工具链被显式选择,模拟器目标和 SDK 已验证。
  • 依赖与缓存,2 分:运行时有版本输出,缓存键区分系统、架构、Xcode 和锁文件。
  • 签名与回退,2 分:归档可验证,旧环境回退任务真实演练过。

8—10 分:普通测试和非关键构建可以迁移到 macOS 26。
5—7 分:先固定系统与 Xcode,继续并行验证,不要迁移生产发布 Job。
0—4 分:回退到明确版本标签,先修复环境隐式依赖,再重新安排迁移。

托管 Runner 与自托管 Mac 的条件分支

不要把“自托管”理解成自动更稳定。它解决的是环境持久性、工具链控制和缓存复用问题,同时增加了权限隔离、系统更新、Runner 更新、磁盘清理和仓库访问控制责任。

按以下条件选择:

  • 若任务是无状态测试,能够接受每次重新准备环境,则选择托管 macOS Runner,固定明确标签并保留迁移矩阵。
  • 若任务依赖固定 Xcode、固定签名身份或本地缓存,则至少把发布 Job 与普通测试 Job 分离;必要时增加独立远程 Mac 自托管 Runner。
  • 若需要同时验证 Apple Silicon 与 Intel,则保留两个明确架构的任务,不要用一个 macos-latest 代替架构矩阵。
  • 若仓库包含不受信任的外部拉取请求,则不要直接把自托管 Runner 暴露给这些工作流。官方安全文档警告,自托管节点不是每次任务后都会自动恢复的干净虚拟机,恶意代码可能持久留在环境中。(GitHub 自托管 Runner 安全与注册说明)
  • 若使用私有仓库并能实施 Runner 分组、最小权限和专用工作流,则可以评估长期在线节点。访问范围应按仓库和工作流限制,而不是让节点接收所有任务。

自托管节点还要验收在线状态、标签匹配和队列行为。匹配的 Runner 若在 60 秒内未接收任务,任务会重新排队;如果没有在线且空闲的匹配节点,任务会持续排队,超过 24 小时可能失败。

如果你准备把远程 Mac 接入日常维护,可以先阅读 SSHMac 的帮助中心,再根据团队的远程访问方式查看远程 Mac 的 VNC 连接说明。这里的重点不是把所有构建都搬走,而是为固定工具链、签名和长期缓存准备一个可控的执行边界。

托管 Runner 的缺点是环境是短生命周期的、默认标签会发生迁移、缓存和工具链需要每次重新确认;完全自建实体 Mac 的缺点则是采购、闲置、系统维护和故障替换都由团队承担。若你的发布流程必须固定 Xcode、签名身份和构建缓存,租赁 SSHMac 的独立远程 Mac 节点通常比继续依赖漂移的 macos-latest 更容易控制;若只是偶发测试或短期验证,则没有必要为此长期保留一台机器。