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。它不表达语义,只保证唯一性和可排序性,适合作为内部追踪号、镜像的附加标签或制品仓库中的唯一键。构建号不应单独作为对外发布版本,否则用户无法从版本号判断兼容性。

选择建议:对外发布的库、SDK、API 服务优先用 SemVer;与时间强绑定的内部交付物可用 CalVer;构建号始终作为辅助标识存在,用于唯一化和追溯,不替代语义版本。

把规则落到 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 标签/分支到最终部署的版本传播路径,以及校验发生的位置:

版本输入源 Git 标签 v2.4.1 / v2.4.1-rc.1 分支提交 main / feature/* 流水线 IID CI_PIPELINE_IID 版本校验 SemVer 正则匹配 标签与包内版本一致 受保护标签权限 预发布标识合法性 不通过 → 流水线失败 版本生成与传播 APP_VERSION=2.4.1-rc.1 镜像标签 app:2.4.1-rc.1 Helm Chart version / appVersion 部署清单 image: ...:2.4.1-rc.1 通过 分支构建版本形态(无标签时) 基线:git describe --tags → 最近标签 v2.4.0 追加:-dev.${CI_PIPELINE_IID} +${CI_COMMIT_SHORT_SHA} 结果:2.4.0-dev.318+a1b2c3d 唯一、可排序、可回溯

图中左侧是三类输入源,中间是校验关卡,右侧是版本号在制品链上的传播结果。需要特别注意的是:校验不通过时流水线必须失败,而不是“警告后继续”。否则一个格式错误的标签仍可能产出镜像,后续再想清理就要面对已经推送到仓库的脏标签。

团队落地时的几个约定

第一,标签统一带 v 前缀,包内版本字段不带 v,转换规则写进校验脚本,避免两套写法混用。第二,预发布标识使用固定词表,例如只允许 alpha、beta、rc,并带递增序号,禁止 -test、-new 这类含义模糊的标识。第三,构建号只用于内部,对外版本号中不出现流水线 IID,避免用户把内部编号误读为语义版本。第四,回滚以标签为准,部署清单中引用的镜像标签必须是不可变的完整版本号,禁止在生产环境使用 latest 或仅用分支名作为标签。

常见错误:在分支上直接打正式标签并推送,导致同一版本号对应多个提交;或者用 CI_COMMIT_SHA 作为唯一版本号,丢失了语义信息,用户无法判断兼容性。正确做法是标签驱动正式版本、分支驱动预发布版本、构建号只做辅助。

把上述规则固化为 CI 模板后,团队每次发布只需要做一件事:在通过测试的提交上打一个符合规范的标签。版本号的计算、校验、传播和记录全部由流水线完成,人工不再手写版本号,也就消除了版本错位的主要来源。