远程 Mac 上的 Agent 已经显示在线,但第一条 Xcode 构建仍然失败,或者重启后节点不再接任务。
最快的处理方式是:可以部署,但必须使用满足 Xcode 27 官方要求的 Apple Silicon Mac,并把 Beta 节点与稳定生产环境隔离;先验收命令行构建,再接入 Simulator、签名和重启恢复。
最后更新于 2026 年 8 月 31 日,兼容性与工具链状态核实自 Apple 的 Xcode 27 Beta Release Notes、Xcode 系统与 SDK 要求页面 以及 Buildkite 官方 Agent 文档。
这篇文章适合三类人:
- 需要把 iOS 或 macOS 构建从本地电脑迁入 Buildkite 的移动开发者;
- 需要维护可远程访问、长期在线构建节点的 DevOps 工程师;
- 正在隔离验证 Xcode 27 Beta,同时必须保留稳定生产流水线的研发平台负责人。
SECTION 01 先确定节点边界:不要把 Beta Mac 直接放进生产队列
Xcode 27 目前仍应按测试版工具链管理。Apple 官方说明显示,Xcode 27 Beta 只能安装并运行在 Apple Silicon Mac 上,并要求 macOS Tahoe 26.4 或更高版本;系统要求页面当前列出的 Xcode 27 beta 6 也对应这一兼容边界。(developer.apple.com)
因此,Intel Mac、系统版本不满足要求的 Mac,或者与稳定版 Xcode 共用同一工作区的节点,都不应直接进入生产队列。不满足芯片或系统硬门槛时,停止安装,不要试图通过 Rosetta、软链接或修改环境变量绕过检查。
Buildkite 的职责是提供控制平面、流水线编排、队列和任务分发;远程 Mac 的职责是运行 Agent、执行 Shell 命令、调用 Xcode 工具链并保存构建产物。自托管 Agent 通过出站连接注册并领取任务,通常不需要为了接收任务而给远程 Mac 随意开放入站端口;真正需要开放的远程管理入口,应由你的 SSH、VNC 或网页控制台方案单独控制。可先核对 Buildkite 自托管 Agent 的官方工作方式。
按工作负载拆分验收范围
| 工作负载 | 必要组件 | 首轮验收目标 | 不应直接推导出的结论 |
|---|---|---|---|
| 纯命令行构建 | Apple Silicon、Xcode 27、依赖工具、代码访问 | xcodebuild 成功并生成结果文件 |
不能证明 Simulator 可用 |
| Simulator 测试 | 命令行环境、Simulator Runtime、用户会话 | 模拟器启动、测试执行、结果收集、清理完成 | 不能证明真机测试通过 |
| 签名与发布 | 证书、私钥、描述文件、受控钥匙串 | 非交互签名成功,产物可验证 | 不能证明普通 PR 可以访问发布资产 |
本篇的判断顺序很简单:先把节点当作命令行构建机,再把它升级为图形测试机,最后才允许它接触签名和发布凭据。这样做不是增加流程,而是把失败范围限制在可回滚的阶段。
远程 Mac 能否直接承载 Buildkite Agent?
可以。Buildkite 官方支持在 macOS 11 Big Sur 或更高版本安装 Agent,并说明 Agent 会以启动 launchd 服务的用户身份运行。也就是说,远程 Mac 本身不是问题,真正的边界在于系统版本、Agent 账户、Xcode 工具链以及任务所需的用户会话是否一致。(buildkite.com)
SECTION 02 第一个里程碑:用专用账户完成注册和队列路由
不要用管理员账户或日常登录账户运行 Buildkite self-hosted agent。建议创建一个只服务于 CI 的 macOS 用户,例如:
CI_USER="<CI专用账户>"
CI_HOME="/Users/<CI专用账户>"
该账户需要拥有构建目录、依赖缓存和必要开发工具的使用权限,但不应默认拥有生产签名资产、个人 SSH 密钥或其他用户目录的读取权限。Buildkite 文档明确指出,macOS Agent 的实际运行用户就是启动 launchd 服务的用户,因此账户选择会直接影响 HOME、钥匙串、SSH 配置和图形会话。(buildkite.com)
第一小时执行顺序
- 在远程 Mac 上确认芯片和系统:
bash
uname -m
sw_vers
预期分别看到 Apple Silicon 对应架构,以及满足 Xcode 27 要求的系统版本。命令输出应保存到部署记录中。
- 使用专用账户安装 Agent。若通过 Homebrew 安装,安装命令可写成:
bash
brew install buildkite/buildkite/buildkite-agent
不要把真实 Token 直接写进公开脚本、Shell 历史或流水线日志。Buildkite 官方建议为不同环境创建专用 Agent Token,并通过安全存储或环境变量注入。
- 在目标 Cluster 中创建专用 Queue,例如:
text
<XCODE27_QUEUE>
一个 Beta 节点只监听一个明确队列,不要让它落入默认队列。Buildkite 的 Queue 文档说明,Agent 可以通过 queue 标签路由到指定自托管队列,而流水线步骤也可以用 agents 属性选择目标队列。(buildkite.com)
- 使用占位符启动注册:
```bash
export BUILDKITE_AGENT_TOKEN="
buildkite-agent start ```
- 提交最小诊断任务,只输出系统、架构和开发工具路径:
```bash set -euo pipefail
uname -a sw_vers uname -m xcode-select -p xcodebuild -version ```
- 在 Buildkite 页面检查该任务实际落到目标 Queue,而不是仅检查某个 Agent 是否显示在线。
怎样让任务只落到 Xcode 27 节点?
不要只在脚本里执行一次 xcode-select,而应同时使用专用 Queue 或 Agent 标签进行路由,再在任务开始时显式选择 Xcode 27 路径。这样可以避免任务被分配到稳定版节点,也能让 Xcode 版本成为可审计的节点属性。
例如,Pipeline 可以采用占位符:
steps:
- label: "Xcode 27 command-line build"
agents:
queue: "<XCODE27_QUEUE>"
command:
- 'sudo xcode-select --switch "/Applications/<XCODE27_APP>.app/Contents/Developer"'
- 'xcodebuild -version'
- './ci/build.sh'
实际部署时不应允许普通开发者通过任意参数覆盖 Queue、证书路径或签名账户。Queue 的作用是隔离工作负载,不能代替权限控制;Token、代码访问和秘密数据仍需分别管理。
| 配置对象 | 建议做法 | 失败时先查什么 |
|---|---|---|
| Cluster | 为 Beta 工具链建立独立 Cluster,或至少使用独立 Queue | Token 是否属于正确 Cluster |
| Queue | 使用 <XCODE27_QUEUE> 等明确名称 |
Pipeline 的 agents.queue 是否拼写一致 |
| Agent Token | 每个环境使用专用 Token,并支持轮换 | Token 是否失效、是否绑定错误 Cluster |
| Agent 用户 | 使用专用低权限账户 | HOME、SSH、钥匙串是否属于该账户 |
| 工作目录 | 为 Xcode 27 节点设置独立构建路径 | DerivedData 是否与其他任务冲突 |
SECTION 03 第二个里程碑:先跑通真实项目的命令行闭环
Agent 在线不等于节点可用。真正的通过证据应该来自一个真实项目:代码能被取到,依赖能解析,Xcode 能编译,单元测试能执行,结果文件能保存,失败时退出码能被 Buildkite 正确识别。
代码访问采用最小权限
如果仓库是私有的,优先使用可轮换的机器身份或 Buildkite Secrets,而不是把个人账户的长期私钥复制到远程 Mac。Buildkite 官方将 SSH 私钥存入 Secrets、并在具体步骤中通过 checkout.ssh_secret 引用列为推荐方式;该设置是步骤级配置,不能假设流水线级配置会自动继承。(buildkite.com)
示例只保留占位符:
steps:
- label: "Checkout and build"
agents:
queue: "<XCODE27_QUEUE>"
checkout:
ssh_secret: "<CI_REPOSITORY_KEY>"
command: "./ci/build-xcode27.sh"
在专用账户下先验证:
git ls-remote "<仓库地址占位符>"
如果代码访问失败,不要立即修改整个远程 Mac 的 SSH 配置。先确认任务到底使用了哪个账户、哪个 HOME 和哪一个秘密名称,再决定是修正步骤级 Secret、机器级密钥,还是仓库权限。
显式固定 Xcode 路径与输出位置
项目脚本不应依赖交互式 Shell 中的隐式变量,例如你在 SSH 会话里设置过 DEVELOPER_DIR,但 launchd 启动的 Agent 并不会自动继承它。
建议在构建脚本开头明确指定:
#!/bin/zsh
set -euo pipefail
export DEVELOPER_DIR="/Applications/<XCODE27_APP>.app/Contents/Developer"
WORKSPACE="<WORKSPACE_PATH>"
SCHEME="<SCHEME_NAME>"
DESTINATION="generic/platform=iOS"
RESULT_PATH="$PWD/artifacts/<RESULT_BUNDLE>.xcresult"
xcodebuild \
-workspace "$WORKSPACE" \
-scheme "$SCHEME" \
-destination "$DESTINATION" \
-resultBundlePath "$RESULT_PATH" \
clean build test
首次执行时,建议拆成四次可观察步骤,而不是把所有动作压成一条长命令:
- 依赖解析;
- 编译;
- 单元测试;
- 结果文件与产物归档。
每一步都记录退出码、日志路径和产物路径。xcresult 是否生成、归档是否能被后续任务读取,比终端里出现“Build Succeeded”更适合作为验收证据。
你还应检查以下隐性成本:
- 缓存污染:不同 Xcode 版本共用 DerivedData,可能产生难以复现的编译结果;
- 环境漂移:Homebrew、Ruby、Node.js 或脚本依赖被自动升级后,原本成功的流水线可能改变行为;
- 磁盘增长:Simulator Runtime、归档文件、依赖缓存和失败工作区会持续占用空间;
- 权限不一致:手动 SSH 执行成功,不代表
launchd下的 Agent 用户也能访问同样目录; - 并发争用:多个任务共用 DerivedData、Simulator 或钥匙串时,失败可能表现为随机超时,而不是明确的配置错误。
SECTION 04 第三个里程碑:只有需要时才接入 Simulator
纯命令行构建节点不应无条件安装并启用图形会话。Simulator、UI 测试和部分开发工具需要用户会话、图形服务、已安装的 Runtime 以及可见的模拟设备;这些条件会让无人值守重启、远程桌面和故障恢复变得更复杂。
Xcode 27 的官方发布说明列出了 Simulator 设备显示、并行输出延迟等已知问题,因此 Beta 节点必须把“测试任务成功”与“生产稳定性”分开记录,不能因为一次 UI 测试通过就把整条流水线切换过去。
最小图形任务的验收顺序
- 确认当前 Agent 运行账户与预期的图形会话账户一致:
bash
id
echo "$HOME"
launchctl print "user/$(id -u)"
- 检查 Xcode 可见的模拟器设备:
bash
xcrun simctl list devices available
-
在流水线中指定一个明确的 Simulator 目标,不要使用“任意可用设备”。
-
执行一个最小单元测试或 UI 测试,记录模拟器启动、测试执行和结果收集三个阶段。
-
测试结束后清理或关闭模拟器,并确认下一次任务不会继承异常状态。
-
连续执行两次相同任务,比较结果文件、退出码和日志结构。
远程自托管 Mac 是否适合执行 iOS Simulator 测试?
可以,但是否可用取决于远程 Mac 的 Xcode Runtime、用户会话、模拟器状态和项目测试类型。Buildkite 只负责把任务交给 Agent;Simulator 能否启动、是否能被当前用户看见,以及 UI 测试是否在无人值守会话中稳定运行,都必须在你的真实节点上验证。
不要把 Simulator 的通过结果扩大解释为真机测试或发布验收。真机连接、设备配对、推送权限、通知行为和 App Store 发布流程,仍然需要单独的测试边界。
SECTION 05 第四个里程碑:把签名发布从普通构建中隔离
签名环境是远程 Mac 部署中最容易被低估的风险点。普通 Pull Request 需要编译和测试,但通常不应接触生产证书、私钥或发布凭据;如果同一个账户、同一个 Queue 和同一个钥匙串同时服务普通构建与发布任务,仓库脚本或恶意依赖就可能获得超出任务需要的权限。
Apple 关于 CI 环境代码签名的说明特别强调:SSH 会话不会自动解锁登录钥匙串,代码签名任务应显式处理钥匙串解锁、私钥访问和非交互授权,并且不建议使用 root 进行签名。(developer.apple.com)
建议采用以下隔离方式:
- 普通构建使用
<BUILD_QUEUE>,发布任务使用<RELEASE_QUEUE>; - 普通构建账户与发布账户分离,至少让发布步骤拥有更窄的访问范围;
- 证书名称、Team ID、密码、描述文件和仓库信息全部使用占位符;
- 发布任务只在受保护分支、受控审批或专用步骤中运行;
- 钥匙串只在任务需要时解锁,任务结束后锁定或销毁临时工作区;
- 先在测试证书和测试应用上完成非交互验证,再接入生产签名资产。
示例:
#!/bin/zsh
set -euo pipefail
KEYCHAIN_PATH="$HOME/Library/Keychains/<CI_KEYCHAIN>.keychain-db"
KEYCHAIN_PASSWORD="<KEYCHAIN_PASSWORD>"
IDENTITY="<SIGNING_IDENTITY>"
TEAM_ID="<TEAM_ID>"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
xcodebuild \
-workspace "<WORKSPACE_PATH>" \
-scheme "<RELEASE_SCHEME>" \
-destination "generic/platform=iOS" \
DEVELOPMENT_TEAM="$TEAM_ID" \
CODE_SIGN_IDENTITY="$IDENTITY" \
archive
这里的命令只是结构示例,不应把密码写死在仓库。若你的安全策略无法证明普通任务不会读取发布钥匙串,先不要开启签名队列。
并发边界必须用真实任务确认
在单台 Mac 上增加第二个 Agent 进程,并不会自动带来可用的并行能力。你需要验证:
- 两个任务是否写入同一 DerivedData;
- 两个任务是否同时启动或重置同一个 Simulator;
- 两个任务是否争用同一钥匙串;
- 归档文件和临时目录是否互相覆盖;
- CPU、内存、磁盘和网络是否出现可观察的排队或失败。
没有隔离证据时,优先让 Xcode 构建、Simulator 和签名任务串行执行。关于自托管节点的并发与扩展,可进一步参考自托管 Mac 构建节点的并发判断思路;不要只根据 Agent 数量决定扩容。
SECTION 06 第五个里程碑:用 launchd 验收重启后的自动上线
重启远程 Mac 后,怎样让 Agent 自动恢复接单?
关键不是手动执行一次 buildkite-agent start,而是让 Agent 由 macOS 的 launchd 按正确用户和会话启动,并在重启后验证它确实重新注册到目标 Queue。Buildkite 官方 macOS 安装文档提供了通过 Homebrew 配置登录启动的方式;Apple 的系统服务文档则说明,用户级 Launch Agent 运行在当前登录用户上下文中,系统级 Launch Daemon 与用户图形会话并不等价。(developer.apple.com)
验收时不要只看进程是否存在,按下面顺序执行一次受控重启:
- 重启前记录当前 Agent 名称、Queue、工作目录和 Xcode 路径。
- 确认没有正在写入的签名任务或未归档的构建任务。
- 执行重启,并从 SSH、VNC 或网页控制台验证远程访问恢复。
- 确认 CI 专用用户会话状态符合 Simulator 或签名任务需要。
- 检查
launchd是否加载了 Agent 服务。 - 在 Buildkite 页面确认 Agent 重新注册到正确 Queue。
- 提交一条真实命令行构建任务。
- 如果该节点承担图形任务,再执行一次最小 Simulator 测试。
- 最后才执行签名或发布类验收。
可以使用以下检查命令:
launchctl list | grep -i "<AGENT_LABEL>"
pgrep -af buildkite-agent
xcode-select -p
xcodebuild -version
df -h
如果远程访问恢复,但 Agent 没有上线,优先检查用户会话、Token、Queue 和日志路径;如果 Agent 在线但 Simulator 失败,则检查图形会话和 Runtime;如果构建成功但签名失败,则回到钥匙串、私钥授权和账户隔离,不要反复重装 Agent。
SECTION 07 上线首周:把节点当成需要维护的硬件资产
远程 Mac 不是一次性安装的软件,而是一台持续消耗磁盘、缓存和维护时间的构建设备。上线第一周至少观察以下项目:
- Agent 日志是否持续增长,是否有轮转策略;
- 构建工作区是否在任务后清理;
DerivedData、归档文件和 Simulator 数据是否有保留上限;- Xcode 27 Beta 是否固定版本,谁负责升级与回滚;
- Agent Token 是否有轮换计划;
- 失败任务能否保留足够日志而不泄露 Token、证书或私钥;
- 生产 Queue 是否仍然只接收稳定工具链;
- Beta 节点是否有明确的回滚入口。
Buildkite 文档说明,自托管 Agent 的基础设施、扩展和更新由你负责;Token 也需要通过撤销或设置过期时间进行生命周期管理。
上线前可勾选验收清单
- [ ] 远程 Mac 已确认是 Apple Silicon,系统版本满足 Xcode 27 官方要求。
- [ ] Xcode 27 Beta 与稳定生产版工具链使用不同节点或不同 Queue。
- [ ] Agent 使用专用低权限 macOS 账户运行。
- [ ] Agent Token 未出现在仓库、命令历史或构建日志中。
- [ ] Agent 已注册到明确的 Cluster 与
<XCODE27_QUEUE>。 - [ ] 最小任务能输出系统、架构、Xcode 路径和版本。
- [ ] 真实项目已完成依赖解析、编译、单元测试和结果文件生成。
- [ ] 项目脚本没有依赖交互式 Shell 的隐式环境变量。
- [ ] Simulator 任务已单独验证启动、执行、结果收集和清理。
- [ ] 普通构建任务无法读取生产签名私钥。
- [ ] 签名任务已验证钥匙串解锁、私钥授权和非交互执行。
- [ ] 单节点并发是否安全已有真实任务证据;没有证据时保持串行。
- [ ] 已执行一次受控重启,并验证远程访问、用户会话、
launchd、Queue 和真实构建。 - [ ] 已记录 Xcode、Agent、依赖和系统版本,保留稳定回滚节点。
如果这份清单中有任何一项只能回答“理论上应该可以”,节点就不应直接进入生产发布队列。
SECTION 08 远程 Mac 与自建 Mac mini 方案,应该怎么选
如果你已有长期稳定、具备物理设备连接需求的 Apple Silicon Mac,自建节点通常更适合持续重负载和固定网络环境;你需要自行承担硬件采购、磁盘更换、系统升级、远程访问、断电恢复和签名资产维护。
如果当前方案是 Windows 或 Linux 主机再叠加虚拟化、远程桌面或临时转发,它的真实缺点通常是:不能完整替代 Xcode 所需的 macOS 工具链,图形会话和 Simulator 更难稳定恢复,Apple Silicon 与 Xcode 27 的兼容边界也更难按官方要求固定;如果直接购买 Mac mini,则会增加一次性硬件成本,并且临时 Beta 验证结束后可能出现设备闲置。
因此,当你的目标是短期验证 Xcode 27、隔离一条 macOS CI 流水线,或暂时缺少可长期在线的 Apple Silicon Mac 时,VPSNIX 的远程 Mac 租赁更适合作为先验收、后决定的方案。你可以先按本文的 Queue、签名隔离、Simulator 和重启标准验证节点,再根据任务周期查看 VPSNIX 的远程 Mac 方案;如果你还需要处理远程访问恢复,可结合远程 Mac 重启后的连接恢复方法设计自己的运维入口。
当团队已经确认需要全年稳定重载、专用物理接口或固定硬件资产时,自购 Mac 并自行维护仍然合理;但如果需求是临时算力、Beta 工具链验证或隔离测试环境,先租用一台真实 Apple Silicon Mac,往往比把不稳定的虚拟化方案直接推进生产更容易控制风险。