团队把 xcodebuild analyze 接进云端 Mac 后,最常见的结果不是立即发现零缺陷,而是一次出现几十条历史诊断。若把告警总数直接设为失败条件,流水线会长期变红;若只把日志留作附件,又没人持续查看。更可执行的办法是先建立经过审阅的基线,再让每次合并只对新增问题负责。
先固定分析任务的输入
静态分析依赖工程配置、编译条件和实际参与编译的源文件。分析前应固定 Xcode 选择、工作区、Scheme、配置与目标平台,并确保 CI 使用与归档任务相同的依赖锁文件。不要用开发者个人 Scheme,也不要在脚本中自动选择“最新”工具链。
分析任务应使用独立目录,避免与编译、测试任务同时改写 DerivedData:
set -o pipefail
ROOT="$PWD"
OUT="$ROOT/ci-artifacts/analyze"
DERIVED="$ROOT/.derived/analyze"
RESULT="$OUT/AppAnalyze.xcresult"
rm -rf "$OUT" "$DERIVED"
mkdir -p "$OUT" "$DERIVED"
xcodebuild analyze \
-workspace App.xcworkspace \
-scheme App \
-configuration Debug \
-destination 'generic/platform=iOS' \
-derivedDataPath "$DERIVED" \
-resultBundlePath "$RESULT" \
CODE_SIGNING_ALLOWED=NO \
| tee "$OUT/xcodebuild.log"
CODE_SIGNING_ALLOWED=NO 只适用于不需要签名才能完成的分析目标。若工程脚本明确读取签名相关构建设置,应先修正脚本的输入边界,而不是盲目追加更多覆盖参数。
基线不是“这些问题可以忽略”的名单,而是迁移期间对已知事实的快照。新增问题仍应立即失败,旧问题则需要负责人和清理计划。
保存完整结果而不是只数 warning
直接执行 grep -c warning: 很方便,但不适合作为长期门禁。日志中的路径、行号和格式可能变化,同一诊断也可能打印多次。CI 应完整保存 .xcresult 和原始日志,再从结构化结果生成一份用于比较的诊断清单。
不同 Xcode 版本的 xcresulttool 子命令可能调整,因此先记录帮助信息并在节点镜像更新时复核解析脚本:
xcrun xcresulttool help > "$OUT/xcresulttool-help.txt"
xcrun xcresulttool get \
--legacy \
--path "$RESULT" \
--format json > "$OUT/xcresult.json"
如果当前工具链不接受 --legacy,应按该版本帮助信息调整命令,并把解析器变更与工具链升级放在同一个合并请求中。不要在脚本失败时退回日志计数并继续放行,否则门禁会悄悄失效。
建议为每条诊断保留以下字段:
| 字段 | 用途 |
|---|---|
| 规则或问题类型 | 区分空指针、资源泄漏等类别 |
| 仓库相对路径 | 避免节点工作目录进入基线 |
| 函数或符号名 | 行号变化后仍能辅助定位 |
| 规范化消息 | 去除临时目录和不稳定数字 |
| 严重级别 | 支持按风险设置处理策略 |
用稳定指纹生成可审阅基线
诊断指纹不应包含绝对路径、DerivedData 路径或单独的行号。较稳妥的组合是“问题类型 + 仓库相对路径 + 符号名 + 规范化消息”。行号可以保留作展示,但不参与主键,否则在文件顶部增加一行就会把所有旧问题识别为新增。
规范化时避免过度合并
可以删除工作目录前缀、临时 UUID 和连续空白,却不要删除变量名、调用名或资源类型。两条发生在同一文件的不同缺陷若被压成同一指纹,后出现的问题会被旧基线遮住。
基线文件建议采用排序后的 JSON Lines,每行一条记录,并纳入版本控制:
{"fingerprint":"sha256:…","type":"AnalyzeWarning","path":"Sources/Cache.swift","symbol":"load()","message":"Potential leak of an object"}
首次建立基线时,应由代码所有者逐条确认。确认内容至少包括:诊断确实来自当前主分支、暂时无法立即修复、已有跟踪事项,并且没有把解析失败或重复记录写进基线。
在合并请求中只比较集合差异
每次任务生成 current.jsonl 后,以指纹集合与 baseline.jsonl 比较。current - baseline 是新增问题,应使任务失败;baseline - current 是已经消失的问题,应提示维护者删除对应基线项,而不是继续保留无效记录。
门禁输出要短而可操作。对每个新增问题打印类型、相对路径、行号、符号和消息,并附上完整 xcresult 的产物位置。控制台日志只展示摘要,避免数千行分析轨迹掩盖真正差异。
区分三种失败
分析脚本至少要区分以下状态:
xcodebuild analyze本身失败,例如依赖缺失或 Scheme 不可用;- 结果解析失败,例如工具链升级后 JSON 结构变化;
- 分析成功,但发现了基线之外的新诊断。
前两种属于基础设施或配置错误,不能显示成“零新增缺陷”。只有分析与解析都成功,集合差异为空时,任务才可以通过。
让基线持续缩小
基线长期不变,最终会成为另一份无人维护的忽略列表。可以按模块指定负责人,在修改相关文件时顺手修复附近诊断;也可以设定每次迭代清理少量高风险项。修复后必须同步删除基线记录,下一次运行会验证问题是否真的消失。
日常检查可按下面的顺序执行:
- 确认工具链、Scheme、配置和目标平台没有漂移;
- 确认分析目录与其他任务隔离;
- 确认 xcresult、日志和规范化清单均已生成;
- 抽查指纹中没有绝对路径或临时标识;
- 新增诊断阻断合并,消失诊断提示收缩基线;
- 工具链升级时先在独立分支重建并审阅差异。
在 OakVM 上运行这类任务时,关键不是选择更激进的阈值,而是保持同一物理节点上的工具链、脚本和结果格式可追踪。只要解析失败不会被当成成功、基线变更必须经过审阅,静态分析就能从一份容易被忽略的长日志,变成稳定的增量质量门禁。
常见问题
为什么不直接要求 Xcode 静态分析零告警?
已有工程可能积累了历史告警,直接要求清零会让门禁长期不可用。先固化基线并只阻断新增缺陷,随后按模块逐步削减基线,更容易持续执行。
静态分析结果应该保存哪些文件?
至少保存完整的 xcresult、构建日志、规范化后的诊断清单和本次使用的基线文件。完整结果用于复查上下文,清单与基线用于稳定比较。
分析任务可以与普通编译共用 DerivedData 吗?
不建议。为 analyze 任务使用独立 DerivedDataPath,可以避免并行任务争抢中间文件,也能减少缓存状态对诊断结果的干扰。
在独享物理节点上运行下一次构建
选择 OakVM M4 或 OakVM M4 Pro,按天、周、月或季租用非虚拟机的云端 Mac。节点实际可用状态以控制台实时返回为准。