3. 版本命名与语义化规范
版本号不是给发布者自己看的备注,而是制品在整条交付链上的身份标识。它同时被包管理器、容器镜像标签、Helm Chart、部署清单、变更记录和回滚脚本消费。一旦命名规则含糊,就会出现“同一个版本号对应两个不同二进制”“回滚时不知道该退到哪个标签”“预发布版本被误当成正式版”等问题。本节要解决的核心问题是:团队先选定一套版本号语法,再把它变成 GitLab CI 中可自动生成、可校验、可追溯的流水线行为。
三种版本号体系各自解决什么问题
SemVer(语义化版本)格式为 MAJOR.MINOR.PATCH,可选预发布标识和构建元数据,例如 2.4.1-rc.3+build.20240611。它的价值在于用版本号本身表达兼容性承诺:MAJOR 递增表示存在不兼容变更,MINOR 递增表示向后兼容地新增功能,PATCH 递增表示向后兼容的问题修复。预发布标识(如 -alpha、-beta、-rc.1)的优先级低于对应的正式版本,因此 1.0.0-rc.1 < 1.0.0。构建元数据 +... 不参与优先级比较,只用于记录来源信息。
CalVer(日历版本)用日期表达版本,常见形式有 YYYY.MM.PATCH、YY.MM.DD、YYYY.MINOR.PATCH。它适合发布节奏与时间强相关、兼容性承诺不强的对象,例如内部工具、数据快照、镜像基线、文档站点。CalVer 的优点是“看到版本号就知道什么时候出的”,缺点是它不天然表达兼容性,因此通常需要配合额外的兼容性说明或 API 版本字段。
构建号(Build Number)是流水线运行产生的单调递增值,例如 GitLab 预定义变量 CI_PIPELINE_IID 或 CI_PIPELINE_ID。它不表达语义,只保证唯一性和可排序性,适合作为内部追踪号、镜像的附加标签或制品仓库中的唯一键。构建号不应单独作为对外发布版本,否则用户无法从版本号判断兼容性。
把规则落到 GitLab:变量、标签与流水线
GitLab 提供了几个关键输入:Git 标签(CI_COMMIT_TAG)、分支名(CI_COMMIT_BRANCH)、提交短 SHA(CI_COMMIT_SHORT_SHA)、流水线 IID(CI_PIPELINE_IID)。版本自动生成的基本思路是:正式版本由标签驱动,预发布版本由分支驱动,构建号始终附加。
下面是一段可直接放进 .gitlab-ci.yml 的版本计算脚本,它根据当前是标签还是分支,生成不同形态的版本号:
stages:
- version
- build
variables:
# 团队约定的版本前缀,便于在制品仓库中区分产品线
VERSION_PREFIX: "app"
compute_version:
stage: version
image: alpine:3.20
script:
- apk add --no-cache git
- |
if [ -n "$CI_COMMIT_TAG" ]; then
# 标签必须是 vX.Y.Z 或 vX.Y.Z-预发布标识
VERSION="${CI_COMMIT_TAG#v}"
echo "正式/预发布版本由标签驱动: $VERSION"
else
# 分支构建:取最近标签作为基线,追加预发布标识与构建号
BASE=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
BASE="${BASE#v}"
VERSION="${BASE}-dev.${CI_PIPELINE_IID}+${CI_COMMIT_SHORT_SHA}"
echo "分支构建版本: $VERSION"
fi
- echo "APP_VERSION=$VERSION" >> version.env
- echo "BUILD_NUMBER=$CI_PIPELINE_IID" >> version.env
artifacts:
reports:
dotenv: version.env
build_image:
stage: build
needs: ["compute_version"]
image: docker:27
script:
- echo "构建镜像标签 app:${APP_VERSION}"
# 实际构建命令示例:
# docker build -t registry.example.com/app:${APP_VERSION} .
# docker push registry.example.com/app:${APP_VERSION}
这段脚本的关键点有三处。第一,正式版本只认标签,避免分支上随手构建出“看起来像正式版”的版本号。第二,分支构建用 git describe --tags --abbrev=0 找到最近的基线标签,再拼上 -dev.<流水线IID> 和提交短 SHA,这样每个分支构建都唯一且可回溯到具体提交。第三,通过 dotenv 制品把版本变量传给后续阶段,避免在多个 job 中重复计算导致不一致。
版本校验:让不合规的标签进不了流水线
自动生成之外,还必须校验。校验的目标是:任何进入构建的版本号都符合团队约定,且标签与提交内容一致。下面是一个用正则做 SemVer 校验的 job,它会在标签推送时先跑,不通过就直接失败:
validate_tag:
stage: version
image: alpine:3.20
rules:
- if: $CI_COMMIT_TAG
script:
- |
TAG="$CI_COMMIT_TAG"
# 允许 v1.2.3 或 v1.2.3-rc.1 或 v1.2.3+build.7
if ! echo "$TAG" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$'; then
echo "标签 $TAG 不符合 SemVer 规范,拒绝构建"
exit 1
fi
echo "标签 $TAG 校验通过"
除了格式校验,还应做两项一致性检查。其一是标签与版本文件一致:如果仓库中有 package.json、pyproject.toml 或 Chart.yaml,应校验其中的版本字段与标签去掉 v 后一致,防止“标签写 1.2.0、包内写 1.1.9”的错位。其二是预发布标签不得指向受保护分支之外的提交,可通过 GitLab 的受保护标签规则(Protected Tags)限制只有 Maintainer 能创建 v* 标签,从权限层面兜底。
版本号在制品链上的传播
版本号一旦生成,就要贯穿镜像、包、Chart、部署清单和变更记录。下图展示了从 Git 标签/分支到最终部署的版本传播路径,以及校验发生的位置:
图中左侧是三类输入源,中间是校验关卡,右侧是版本号在制品链上的传播结果。需要特别注意的是:校验不通过时流水线必须失败,而不是“警告后继续”。否则一个格式错误的标签仍可能产出镜像,后续再想清理就要面对已经推送到仓库的脏标签。
团队落地时的几个约定
第一,标签统一带 v 前缀,包内版本字段不带 v,转换规则写进校验脚本,避免两套写法混用。第二,预发布标识使用固定词表,例如只允许 alpha、beta、rc,并带递增序号,禁止 -test、-new 这类含义模糊的标识。第三,构建号只用于内部,对外版本号中不出现流水线 IID,避免用户把内部编号误读为语义版本。第四,回滚以标签为准,部署清单中引用的镜像标签必须是不可变的完整版本号,禁止在生产环境使用 latest 或仅用分支名作为标签。
CI_COMMIT_SHA 作为唯一版本号,丢失了语义信息,用户无法判断兼容性。正确做法是标签驱动正式版本、分支驱动预发布版本、构建号只做辅助。把上述规则固化为 CI 模板后,团队每次发布只需要做一件事:在通过测试的提交上打一个符合规范的标签。版本号的计算、校验、传播和记录全部由流水线完成,人工不再手写版本号,也就消除了版本错位的主要来源。