工程指南

云端 Mac CI 的 iOS 构建号治理实战

云端 Mac CI 的 iOS 构建号治理实战

两条 iOS 归档流水线同时跑在云端 Mac 上:一条修复线上问题,另一条准备常规发布。它们都从工程文件读到相同的 CURRENT_PROJECT_VERSION,最终生成两个构建号一致、代码却不同的制品。问题通常直到提交发布系统时才暴露,此时仅看文件名和提交记录,很难确认哪一份应该保留。

构建号治理的目标不是“自动加一”,而是让每个可分发制品都能回答三个问题:编号由谁分配、对应哪次提交、归档中的实际值是什么。

先区分版本号与构建号

MARKETING_VERSION 是用户看到的版本,例如 3.8.0CURRENT_PROJECT_VERSION 是构建号,应该是只包含数字的递增值。两者生命周期不同,不要把提交标签直接同时写入两个字段。

字段 示例 变更时机 主要用途
MARKETING_VERSION 3.8.0 产品版本变更 识别功能版本
CURRENT_PROJECT_VERSION 18427 每次可分发归档 区分同版本制品
Git 提交 a1b2c3d 每次提交 定位源代码
流水线编号 5821 每次任务运行 定位执行记录

工程仓库可以保存稳定的版本号,但不应由多个执行器同时修改并提交构建号。并发任务各自执行“读取、加一、写回”时,即使都成功,也可能拿到相同结果。

把构建号视为流水线输入,而不是源码修改结果。源码负责声明如何使用它,调度系统负责保证它唯一。

建立唯一的编号来源

选择可单调递增的序列

正式归档应从一个集中来源领取整数,例如流水线系统的全局运行序号,或由内部协调任务原子分配的序列。编号必须满足三个条件:

  1. 同一发布目标下不重复;
  2. 新编号大于已提交过的编号;
  3. 能反查提交、分支和任务运行记录。

git rev-list --count HEAD 适合不改写历史的单分支项目,但不适合浅克隆、变基或多发布分支。不同分支可能得到相同计数,历史重写也可能让数字倒退。它可以用于内部调试包,不能充当复杂发布流程的唯一事实来源。

在 OpsVM 的并行任务中,可以让调度层先生成 BUILD_SEQUENCE,再把它传入具体构建节点。节点只消费编号,不自行竞争或回写。

在任务入口拒绝脏数据

#!/bin/zsh
set -euo pipefail

: "${BUILD_SEQUENCE:?BUILD_SEQUENCE is required}"
: "${RELEASE_VERSION:?RELEASE_VERSION is required}"

if [[ ! "$BUILD_SEQUENCE" =~ ^[0-9]+$ ]]; then
  print -u2 "BUILD_SEQUENCE must contain digits only"
  exit 64
fi

if [[ ! "$RELEASE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
  print -u2 "RELEASE_VERSION must use major.minor.patch"
  exit 64
fi

入口校验应早于依赖安装和编译。这样,变量缺失不会在十几分钟后变成一个无法分发的归档。

在归档命令中注入编号

无需让流水线修改 project.pbxproj。直接在 xcodebuild 命令末尾覆盖构建设置,既不会制造仓库脏文件,也便于从日志复现。

archive_path="$PWD/output/App.xcarchive"
result_path="$PWD/output/Archive.xcresult"

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Release \
  -destination "generic/platform=iOS" \
  -archivePath "$archive_path" \
  -resultBundlePath "$result_path" \
  MARKETING_VERSION="$RELEASE_VERSION" \
  CURRENT_PROJECT_VERSION="$BUILD_SEQUENCE" \
  clean archive

覆盖项要放在同一次归档命令中,避免测试使用一个编号、归档又读取工程默认值。若工程包含扩展组件,还要确认主应用与扩展是否继承相同设置。除非发布规范明确要求,否则不要为每个 target 独立生成编号。

归档前可以先检查解析后的设置:

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Release \
  -showBuildSettings |
awk '/MARKETING_VERSION|CURRENT_PROJECT_VERSION/ { print }'

这一步主要发现脚本、配置文件或 target 级设置对命令行参数的意外覆盖。

不信日志,直接验收归档

命令返回成功只表示归档完成,不代表目标值已经进入最终应用。验收脚本应读取 .xcarchive 内主应用的 Info.plist,并与流水线输入比较。

app_path="$(find "$archive_path/Products/Applications" \
  -maxdepth 1 -name '*.app' -type d -print -quit)"

if [[ -z "$app_path" ]]; then
  print -u2 "Application bundle not found"
  exit 65
fi

plist="$app_path/Info.plist"
actual_version=$(/usr/libexec/PlistBuddy \
  -c "Print :CFBundleShortVersionString" "$plist")
actual_build=$(/usr/libexec/PlistBuddy \
  -c "Print :CFBundleVersion" "$plist")

[[ "$actual_version" == "$RELEASE_VERSION" ]] || exit 66
[[ "$actual_build" == "$BUILD_SEQUENCE" ]] || exit 67

验收通过后,把版本号、构建号、完整提交哈希、归档校验值和流水线运行标识写入同一份纯文本清单,并与制品一起保存。文件名可以方便人读,但不能代替清单和归档内部字段。

处理重试、分支与并发

重试规则要在流水线层固定

失败发生在编译前,且没有生成归档时,可以复用原编号重试同一任务。若归档已经生成、上传或进入后续处理,就应领取新编号。旧编号保留在记录中,不要为了数字连续而回收。

多条发布分支共享同一发布目标时,也应共享编号空间。按分支分别从 1 开始看似整齐,合流后却会产生碰撞。分支名、提交哈希和版本号负责表达来源,构建号只负责唯一与递增。

最小检查清单

  • 编号由一个集中来源原子分配;
  • 构建节点只读编号,不修改工程文件;
  • 归档命令显式传入两个版本字段;
  • 主应用与扩展的继承关系已经核对;
  • 成功后读取归档内部字段进行比较;
  • 每份制品保存提交、任务和校验值映射;
  • 已产生制品的失败任务不回收编号;
  • 并行节点在控制台确认当前可选配置后,只承担构建,不承担配号。

完成这些约束后,构建号不再是发布前临时修改的数字,而是贯穿源码、执行记录和最终制品的稳定索引。发生回滚或并行任务冲突时,团队可以先凭编号定位归档,再回到唯一的提交与流水线现场。

常见问题

iOS 构建号可以直接使用 Git 提交数量吗?

个人仓库或不会改写历史的单分支流程可以使用;存在变基、浅克隆或多条发布分支时,应改用流水线统一分配的单调递增整数。

流水线重试时应该复用原构建号吗?

仅重试未产生归档的同一任务时可以复用。任务已经生成或提交过制品时,应分配新构建号,并保留它与提交、流水线运行记录的映射。

独享物理节点

在 OpsVM 部署云端 Mac 工作流

从三档 Apple Silicon 配置与 6 个在售节点中完成选择,实际可用状态以控制台实时返回为准。

选择配置并下单