凌晨的自动构建突然失败,终端只留下 Command PhaseScriptExecution failed,重跑却正常。此时继续盯着最后一行没有意义。云端 Mac 上的构建工具、签名服务、网络进程和系统权限会分别写入 macOS 统一日志。把它们放到同一时间轴,通常能判断问题来自项目脚本、系统服务,还是远程连接中断。
先固定故障时间和复现边界
先记录节点时间、命令开始时间、失败时间和退出码。远程客户端显示的本地时间可能与节点时区不同,排查时统一使用节点上的时间。
date -u '+%Y-%m-%dT%H:%M:%SZ'
set -o pipefail
xcodebuild \
-workspace Demo.xcworkspace \
-scheme Demo \
-configuration Release \
build 2>&1 | tee "$HOME/build-output.log"
printf 'exit=%s
' "${PIPESTATUS[0]}"
不要立即清理 DerivedData、重启进程或覆盖原日志。先复制构建输出,再确认故障是否稳定复现。偶发问题至少记录两次运行结果,并注明两次使用的提交、Xcode 路径、Shell 环境和工作目录是否一致。
统一日志不是终端输出的替代品。前者解释系统和服务发生了什么,后者说明构建命令执行到了哪一步。两份记录必须使用同一个时间窗口。
用谓词缩小统一日志范围
log show 适合回看已经发生的事件。不要先导出数小时的全部日志,这会制造大量噪声,也会增加脱敏成本。以失败前后十分钟为边界,先按进程名筛选:
log show \
--last 20m \
--style compact \
--info \
--debug \
--predicate 'process == "xcodebuild" OR process == "codesign"'
如果终端只显示签名失败,可继续搜索安全服务返回的拒绝信息;如果构建过程被远程会话影响,则检查 sshd 和实际执行脚本的进程。谓词字段应尽量使用 process、subsystem、category 和 eventMessage,不要只对整段文本做宽泛匹配。
log show --last 15m --style compact \
--predicate '(process == "sshd") OR (eventMessage CONTAINS[c] "denied")'
常见信号可以按下表解释,但最终判断仍要结合退出码和复现步骤。
| 日志信号 | 优先检查 | 不应直接下的结论 |
|---|---|---|
permission denied |
文件权限、钥匙串访问、执行账户 | 节点硬件故障 |
| 进程被终止 | 内存压力、父进程、脚本超时规则 | Xcode 本身损坏 |
| SSH 会话断开 | 客户端网络、保活参数、任务托管方式 | 构建一定失败 |
| 签名服务无匹配项 | 钥匙串、证书名称、配置文件映射 | 重新安装全部工具链 |
在复现时实时观察关键事件
能够稳定复现时,另开一个 SSH 会话运行 log stream。实时流只承担观察,不要把筛选条件写得过宽,否则关键事件会被连续输出淹没。
log stream \
--style compact \
--level info \
--predicate 'process == "xcodebuild" OR process == "codesign" OR process == "securityd"'
随后在原会话启动构建,并记录第一条异常日志与构建失败之间的时间差。若签名错误先出现,再由构建工具汇总为通用失败,应优先处理签名链路;若 SSH 先断开但构建进程仍在运行,则需要检查任务是否绑定交互式终端,而不是把它误判成编译错误。
为脚本增加可关联标记
自有脚本可以在开始和结束处写入统一日志,使用固定 subsystem 与每次运行唯一的任务编号。这样不必依赖模糊的文件名搜索。
job_id="$(date -u '+%Y%m%dT%H%M%SZ')"
logger -p user.notice -t minid-build "job=$job_id phase=start"
./scripts/build.sh
status=$?
logger -p user.notice -t minid-build "job=$job_id phase=end status=$status"
exit "$status"
logger 写入的标签可通过 process 或消息内容检索。任务编号只用于关联一次运行,不应包含令牌、仓库凭据或客户数据。
导出可复查且可安全传递的证据
命令行文本适合快速分析,复杂问题则应保留 .logarchive。归档前再次确认时间范围,避免收集与故障无关的历史记录。
mkdir -p "$HOME/diagnostics"
sudo log collect \
--last 15m \
--output "$HOME/diagnostics/build-incident.logarchive"
cp "$HOME/build-output.log" "$HOME/diagnostics/"
不要直接把整个目录发送出去。先检查构建日志中的环境变量展开、项目绝对路径、节点地址、仓库地址、证书名称和脚本参数。统一替换敏感值时保留稳定映射,例如把同一用户名始终替换为 <USER>,否则后续无法判断多条事件是否属于同一账户。
建议同时写一份纯文本说明,包含 UTC 时间、执行命令、退出码、预期结果、实际结果、是否可复现,以及故障前最后一次成功运行的提交。日志能证明发生了什么,说明文件则限定调查边界。
按证据决定下一步动作
完成采集后,先把问题归入项目、环境、连接或资源四类。项目类问题可在同一提交上更换干净工作目录验证;环境类问题应核对 Xcode 选择、Shell 初始化和钥匙串可见性;连接类问题要区分会话断开与任务退出;资源类问题则检查磁盘余量、内存压力和并发进程。
排查结束前执行一次最小验收:相同提交连续构建两次,确认退出码为零;签名产物可由 codesign --verify --deep --strict 校验;后台任务在 SSH 断开后仍按预期结束;诊断目录中不存在明文令牌。只有这些条件同时成立,故障才算真正闭环。
常见问题
为什么不能只保存 xcodebuild 的终端输出?
终端输出主要覆盖构建步骤,权限拒绝、签名服务、网络进程和系统组件异常可能只进入统一日志。两类记录应按同一时间点一起保存。
提交日志前必须删除哪些内容?
至少检查用户名、项目绝对路径、节点地址、仓库地址、令牌、证书名称和业务数据。不能确认安全的字段应替换为稳定占位符后再提交。
MiniD Cloud Mac
按天、周或月租用独享物理 Mac mini
整机独享的物理 Mac mini,远程桌面与 SSH 均可接入;具体机型、区域与租期以订购页当前信息为准。