两条 iOS 归档流水线同时跑在云端 Mac 上:一条修复线上问题,另一条准备常规发布。它们都从工程文件读到相同的 CURRENT_PROJECT_VERSION,最终生成两个构建号一致、代码却不同的制品。问题通常直到提交发布系统时才暴露,此时仅看文件名和提交记录,很难确认哪一份应该保留。
构建号治理的目标不是“自动加一”,而是让每个可分发制品都能回答三个问题:编号由谁分配、对应哪次提交、归档中的实际值是什么。
先区分版本号与构建号
MARKETING_VERSION 是用户看到的版本,例如 3.8.0;CURRENT_PROJECT_VERSION 是构建号,应该是只包含数字的递增值。两者生命周期不同,不要把提交标签直接同时写入两个字段。
| 字段 | 示例 | 变更时机 | 主要用途 |
|---|---|---|---|
MARKETING_VERSION |
3.8.0 |
产品版本变更 | 识别功能版本 |
CURRENT_PROJECT_VERSION |
18427 |
每次可分发归档 | 区分同版本制品 |
| Git 提交 | a1b2c3d |
每次提交 | 定位源代码 |
| 流水线编号 | 5821 |
每次任务运行 | 定位执行记录 |
工程仓库可以保存稳定的版本号,但不应由多个执行器同时修改并提交构建号。并发任务各自执行“读取、加一、写回”时,即使都成功,也可能拿到相同结果。
把构建号视为流水线输入,而不是源码修改结果。源码负责声明如何使用它,调度系统负责保证它唯一。
建立唯一的编号来源
选择可单调递增的序列
正式归档应从一个集中来源领取整数,例如流水线系统的全局运行序号,或由内部协调任务原子分配的序列。编号必须满足三个条件:
- 同一发布目标下不重复;
- 新编号大于已提交过的编号;
- 能反查提交、分支和任务运行记录。
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 个在售节点中完成选择,实际可用状态以控制台实时返回为准。