症状:工作流已经写好,Runner 也显示在线,但任务仍然排队、调用了错误的 Xcode,或者 Archive 成功后迟迟看不到 TestFlight 构建。

最快解法:GitHub Actions iOS 打包不能只注册 Runner 就投入生产,必须同时验收主机可用性、Xcode 基线、签名隔离、任务路由和重启恢复。

这篇教程适合哪些开发者

这篇内容适合已经使用 GitHub 托管代码,希望自动完成 iOS 构建、签名和发布的独立开发者,也适合没有本地 Mac、准备把远程 macOS 主机改造成常驻打包机的 Windows 或 Linux 开发者。

如果团队只想偶尔手动打一个测试包,完整的常驻 Runner 方案可能过重;如果需要固定原生依赖、持续集成和 TestFlight 发布,下面的验收方法更适合长期使用。

先用 5 个指标判断方案是否合格

GitHub Actions 可以调度远程 Mac 上的 self-hosted runner,但“Runner 在线”只说明它能连接调度平台,不代表已经具备生产发布能力。一个可用的 iOS 打包环境,至少要同时满足以下条件:

  • ✅ 主机能够持续运行,网络连接稳定,远程会话断开后任务不会随之停止。
  • ✅ Runner 具备明确的 macOSARM64 和发布用途标签。
  • ✅ Xcode、macOS、SDK、依赖锁文件和构建 Scheme 已固定。
  • ✅ 代码签名证书、私钥、Provisioning Profile 和上传凭据没有写入仓库。
  • ✅ 真实项目能够分别通过编译、Archive、签名导出、上传和后台处理验收。

GitHub 官方支持把自托管 Runner 注册到仓库、组织或企业层级;注册时生成的令牌是限时令牌,官方文档说明其有效期为 1 小时,因此不能把旧令牌长期保存在脚本或密码管理器中反复使用。添加 self-hosted runner 的官方步骤

决策维度 短期测试 Runner 常驻发布 Runner
适合场景 验证项目能否在远程 Mac 编译 持续执行 Archive、签名和 TestFlight 上传
主机要求 临时可用、手动启动也可以 重启后自动恢复,长期保持在线
权限策略 可使用测试项目和测试凭据 仅限私有仓库、受控分支和发布工作流
工具链管理 先确认能否编译 固定 Xcode、SDK、依赖和 Scheme
验收标准 编译成功 编译、Archive、导出、上传、后台处理全部可追踪
是否适合直接生产 ❌ 不建议 ✅ 通过完整验收后才适合

远程 Mac 需要先满足哪些条件?

先核对 macOS、Xcode 与 SDK 的组合

远程 Mac 不是普通 Linux 构建机。Xcode 对 macOS 版本、SDK、设备支持和模拟器都有对应限制,主机系统不满足要求时,Runner 即使在线,也可能在安装工具链或启动构建阶段失败。

写作时 Apple 官方系统要求页面列出:Xcode 26.6 支持 macOS Tahoe 26.2 至 macOS Tahoe 26.x,并包含 iOS 26.5 SDK;Apple 另行说明,自 2026 年 4 月 28 日起,上传到 App Store Connect 的 App 必须使用 Xcode 26 或更高版本,并采用 iOS 26 等对应 SDK 构建。正式配置前,应以当前官方系统要求页重新核对,而不是只看 Runner 的操作系统标签。Apple Xcode 系统要求与上传要求

主机验收时至少检查:

sw_vers
xcode-select -p
xcodebuild -version
xcodebuild -showsdks
uname -m
df -h

重点不是把所有 Xcode 版本都安装到同一台主机,而是确认当前项目实际调用的开发者目录、SDK 和架构。若一台远程 Mac 同时存在多个 Xcode,工作流必须显式设置:

sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
xcodebuild -version

路径中的 Xcode.app 只是示例占位符。如果实际安装目录不同,应替换成经过人工确认的路径,不能把不存在的目录直接写进生产工作流。

按仓库或组织范围注册 Runner

在仓库的设置页面创建 self-hosted runner 后,GitHub 会根据操作系统和架构显示安装命令。下载 Runner 后,在远程 Mac 上执行官方生成的配置命令;示例中的地址、令牌和目录均使用占位符:

mkdir -p ~/actions-runner
cd ~/actions-runner

# 下载并解压官方 Runner 程序
# 下载地址、版本和校验值以 GitHub 页面实时生成内容为准

./config.sh \
  --url https://github.com/<OWNER>/<REPOSITORY> \
  --token <RUNNER_REGISTRATION_TOKEN> \
  --name <RUNNER_NAME> \
  --labels self-hosted,macOS,ARM64,ios-release

配置结束后先不要马上运行发布任务,先执行:

./run.sh

终端应出现已连接并等待任务的状态,GitHub 设置页面也应显示 Runner 为 Idle,而不是 Offline。官方文档明确指出,Runner 应用必须处于活动状态,才能接收工作流任务。Runner 状态与故障排查文档

如何固定 Runner 的 Xcode 工具链版本?

用标签锁定 macOS 架构和发布用途

Runner 的默认标签可以帮助工作流筛选操作系统和架构,但默认标签并不能替代工具链检查。建议为生产发布主机添加用途明确的自定义标签,例如:

jobs:
  release:
    runs-on:
      - self-hosted
      - macOS
      - ARM64
      - ios-release

ios-release 的含义应保持单一:只表示这台 Runner 允许执行发布链路。不要使用过于宽泛的 buildtest 等标签,否则普通测试任务也可能抢占发布主机。

GitHub 的标签匹配是“工作流要求的标签全部满足后,任务才会被分配”;标签名称不区分大小写。若没有同时满足 self-hostedmacOSARM64ios-release 的 Runner,任务会继续处于排队状态,而不是自动切换到不符合条件的主机。GitHub 标签路由说明

随后在工作流中固定工具链,而不是相信主机当前的默认设置:

steps:
  - name: Select Xcode
    run: |
      sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
      xcodebuild -version
      xcodebuild -showsdks

  - name: Restore dependencies
    run: |
      xcodebuild -resolvePackageDependencies \
        -project <PROJECT_NAME>.xcodeproj \
        -scheme <SCHEME_NAME>

  - name: Build without signing
    run: |
      xcodebuild \
        -project <PROJECT_NAME>.xcodeproj \
        -scheme <SCHEME_NAME> \
        -sdk iphoneos \
        -configuration Release \
        CODE_SIGNING_ALLOWED=NO \
        build

这一步的目的,是先证明源码、依赖、Scheme 和编译工具链能够重复运行。若不含签名材料的最小构建都不能稳定通过,继续导入证书只会把“编译问题”伪装成“签名问题”。

⚠️ 不要在工作流里把 Xcode 版本写成“最新版”。Xcode 更新可能同时改变 SDK、Swift 编译器和上传边界;生产 Runner 应使用明确路径,并在版本升级前重新执行完整验收。

代码签名材料怎样保存和隔离?

分清 4 类不同材料

安全保存 GitHub Actions 的 iOS 签名证书,重点不是把全部内容塞进一个 Secret,而是先按用途拆分:

  1. 分发证书和私钥:用于对 App 进行签名,私钥通常需要进入临时 Keychain。
  2. Provisioning Profile:必须与 Bundle ID、团队和分发用途匹配。
  3. App Store Connect API Key:用于上传或调用后台接口,不能代替代码签名证书和 Provisioning Profile。
  4. GitHub Actions Secrets:只保存加密后的材料或临时密码,不保存真实文件到代码仓库。

Apple 的文档明确区分了开发证书、分发证书和 Provisioning Profile;用于上传 App Store Connect 的配置文件需要显式 App ID,并包含一个分发证书。Apple 证书与 Provisioning Profile 说明

在仓库中只保留明显脱敏的变量名:

env:
  KEYCHAIN_NAME: ci-temporary.keychain-db
  KEYCHAIN_PASSWORD: <TEMPORARY_KEYCHAIN_PASSWORD>
  ASC_KEY_ID: <APP_STORE_CONNECT_KEY_ID>
  ASC_ISSUER_ID: <APP_STORE_CONNECT_ISSUER_ID>

证书、私钥和 Profile 可以通过 GitHub Secrets 注入到 Runner 的临时目录,再在任务结束时删除。示例不应出现真实的 Bundle ID、Team ID、证书名称、密码、API Key、文件路径或日志内容。

临时 Keychain 解决的是构建完成后减少凭据残留;最小权限解决的是上传账号权限过大;任务后清理解决的是下一次任务读取旧材料;凭据轮换解决的是材料已经暴露后的持续风险。这几项不是同一个控制点,不能用一枚 API Key 替代完整的代码签名材料。

限制发布 Runner 的访问范围

常驻发布主机不应接收任意分支、任意 Pull Request 或公开仓库 Fork 发起的工作流。GitHub 官方建议谨慎使用 self-hosted runner,因为不受信任的代码可能在主机上执行,并接触到环境变量、工作目录或其他凭据。GitHub 自托管 Runner 安全建议

建议采用以下边界:

  • 发布 Runner 只绑定私有仓库或经过严格控制的组织仓库。
  • 只有受保护分支或人工批准的发布工作流才能调用 ios-release 标签。
  • 普通单元测试使用另一组 Runner,不与签名主机混用。
  • 不允许外部贡献者的代码直接触发带有发布凭据的任务。
  • 工作流日志中禁止输出证书内容、私钥、临时密码和完整上传响应。

如果团队需要进一步整理凭据,可以参考 GitHub Actions iOS 签名凭据配置指南;重点不是把 Secret 数量做多,而是让每一份材料只在必须的步骤、必须的主机和必须的权限范围内出现。

把 Archive、导出和 TestFlight 上传拆开验收

用分层任务定位失败位置

GitHub Actions iOS 打包最容易误判的地方,是把 xcodebuild 返回成功等同于“App 已经发布”。实际上,构建、Archive、导出、上传和后台处理是不同阶段,失败原因也不同。

可以按下面的顺序组织工作流:

jobs:
  release:
    runs-on:
      - self-hosted
      - macOS
      - ARM64
      - ios-release
    timeout-minutes: 60

    steps:
      - uses: actions/checkout@v4

      - name: Verify toolchain
        run: |
          xcode-select -p
          xcodebuild -version
          xcodebuild -showsdks

      - name: Resolve dependencies
        run: |
          xcodebuild -resolvePackageDependencies \
            -workspace <WORKSPACE_NAME>.xcworkspace \
            -scheme <SCHEME_NAME>

      - name: Archive
        run: |
          xcodebuild archive \
            -workspace <WORKSPACE_NAME>.xcworkspace \
            -scheme <SCHEME_NAME> \
            -archivePath "$RUNNER_TEMP/<APP_NAME>.xcarchive"

      - name: Export IPA
        run: |
          xcodebuild -exportArchive \
            -archivePath "$RUNNER_TEMP/<APP_NAME>.xcarchive" \
            -exportOptionsPlist <EXPORT_OPTIONS_PLIST_PATH> \
            -exportPath "$RUNNER_TEMP/export"

      - name: Upload to TestFlight
        run: |
          xcrun altool \
            --upload-app \
            -f "$RUNNER_TEMP/export/<APP_NAME>.ipa" \
            -t ios \
            -u <UPLOAD_ACCOUNT_PLACEHOLDER> \
            -p <UPLOAD_PASSWORD_PLACEHOLDER>

示例中的上传参数仅用于展示结构,不能直接复制到生产环境;团队也可以使用 App Store Connect API 和 JWT 方式上传。Apple 说明,上传后构建还需要经过后台处理,处理完成前不会立即出现在 App Store Connect 中,因此“上传命令退出成功”并不代表 TestFlight 已经可以安装。Apple 上传构建说明

为每一层保留不同证据

建议在工作流中分别保存:

  • 依赖恢复日志:确认锁文件、网络和依赖缓存是否正常。
  • 编译日志:确认 Swift、原生依赖和 SDK 是否兼容。
  • .xcarchive:用于检查 Archive 是否生成以及包含哪些架构。
  • 导出日志和 IPA 文件:确认签名导出是否使用了正确的分发配置。
  • 上传日志:确认传输阶段是否成功。
  • 构建编号、上传状态和后台处理结果:确认 TestFlight 是否真正出现构建。

首次上线前,最好使用脱敏测试项目走完整链路,并人为制造一次错误,例如暂时使用不匹配的 Profile。验收记录应能回答:这是工具链错误、签名错误、传输错误,还是 App Store Connect 后台仍在处理,而不是只留下一个“Job failed”。

让远程 Mac 在重启后自动恢复

把交互式启动改成系统服务

直接运行 ./run.sh 适合初次验证,不适合作为常驻打包机。关闭 SSH 或 VNC 会话后,交互式进程可能结束;远程 Mac 重启后,也不会自动恢复。

在 Runner 目录中执行:

cd ~/actions-runner
./svc.sh install
./svc.sh start
./svc.sh status

GitHub 官方说明,macOS Runner 可以通过系统服务在主机启动时自动运行;状态检查应确认服务已经启动,而不是只确认配置文件还存在。GitHub Runner 服务配置文档

重启恢复验收至少包含以下动作:

  1. 记录当前 Runner 名称、标签和注册范围。
  2. 停止交互式 ./run.sh,确认服务可以独立启动。
  3. 重启远程 Mac。
  4. 通过 ./svc.sh status 和 GitHub 设置页面确认 Runner 回到 Idle
  5. 提交一个无签名最小构建,确认重启后可以接单。
  6. 检查 xcode-select、Keychain 权限和工作目录是否仍然正确。
  7. 若主机不再使用,撤销 Runner 注册并删除本地配置。

长期运行还需要检查 Runner 更新、磁盘增长、依赖缓存失效和构建中断后的清理方式。若主机出现离线状态,应区分主机断电、服务停止、Runner 更新失败和访问权限失效,而不是直接重复注册。

上线前的可勾选验收清单

主机与路由

  • [ ] macOS 版本满足目标 Xcode 的官方系统要求。
  • [ ] uname -m 与工作流中的 ARM64 标签一致。
  • [ ] 磁盘空间、网络连接和远程登录方式已验证。
  • [ ] self-hostedmacOSARM64ios-release 标签均能匹配。
  • [ ] 没有匹配 Runner 时,团队能够发现排队状态并触发告警。
  • [ ] 工作流设置了合理的超时,而不是无限等待。

工具链与项目

  • [ ] xcode-select -p 指向经过确认的 Xcode。
  • [ ] 项目依赖锁文件已提交并能够恢复。
  • [ ] Scheme、Configuration 和 SDK 选择固定。
  • [ ] 不含签名材料的最小构建已经通过。
  • [ ] Archive 输出路径不会与并发任务互相覆盖。

凭据与访问

  • [ ] 证书私钥不在仓库、不出现在日志中。
  • [ ] Provisioning Profile 与 Bundle ID 和分发用途一致。
  • [ ] App Store Connect 上传凭据与代码签名材料分开管理。
  • [ ] 临时 Keychain 在任务结束后清理。
  • [ ] 发布 Runner 不接收公开仓库 Fork 或未经审核的外部代码。
  • [ ] Runner 注册、撤销和凭据轮换都有记录。

发布结果

  • [ ] 编译、Archive、导出、上传各自有独立日志。
  • [ ] IPA 文件可以被验收步骤找到。
  • [ ] 上传响应没有被误读为后台处理完成。
  • [ ] TestFlight 构建已经在 App Store Connect 中出现。
  • [ ] 构建编号、版本号和 Bundle ID 与预期一致。
  • [ ] 首次真实发布已经能定位每一层的失败原因。

如果团队还没有可长期运行的 macOS 环境,可以先查看 远程 Mac 持续集成环境验收清单;短期测试阶段不必一开始就承诺常驻运行,先用真实项目完成 Archive、签名和上传,再决定是否扩大到持续集成。

当前方案和远程 Mac,应该怎样做选择

如果现有方案是本地 Windows 或 Linux 加上临时虚拟机,常见缺点是 Xcode 无法直接运行、macOS 版本和原生依赖难以固定,而且主机重启后 Runner、Keychain 和构建环境往往需要人工恢复。若改用公共 CI 环境,工具链选择、缓存行为、并发排队和签名材料边界又可能不完全受团队控制。

对于只需偶尔打包的开发者,本地借用设备或短期测试环境可能更简单;对于需要持续运行 GitHub Actions、固定 Xcode 并保留完整 root 权限的项目,租赁 RUVCLOUD 的远程 Mac 更适合先验证生产链路,再决定是否长期投入自购 Mac。可以根据项目周期查看 RUVCLOUD 的 Mac 方案,先选择短周期完成 Runner 注册、重启恢复和 TestFlight 验收,确认工作流稳定后再调整使用周期。