实战:格式设计、CI集成与踩坑指南)
2021-10-25 这个字符串落到不同人手里会有完全不同的命运。在普通人那里它就是个日期在我这种常年跟发布流程打交道的人眼里它是个版本号而且是个格式相当规整的版本号。日期版本号很多人管它叫 CalVerCalendar Versioning做法就是把发布当天的时间直接写进版本号里形如 2021.10.25 或者 20211025。这篇文章不打算绕弯子就聊聊我是怎么把一个日期字符串真正用进项目里的格式怎么定、CI 怎么接、中途踩过什么坑以及为什么直到现在我仍然觉得日期版本号被严重低估了。如果你正在维护一个持续迭代的应用、内部系统、桌面软件或者经常需要面对“用户报了一个版本号但你根本不知道是什么时候发布的”这种尴尬场景那这篇内容应该能帮你省下不少事。即便你手里的是标准语义化版本号的项目里面关于构建号、时区、发布流程的讨论也同样适用。1. 为什么把日期当版本号从 2021-10-25 说起1.1 第一次看到日期版本号时的直观感受我印象里第一次接触日期版本号是在某个开源项目的 Release 页面版本号写着 2021.10.25当时第一反应是这项目是不是偷懒直接拿发布日期当版本号了后来用得多了才明白越是面向用户、迭代节奏稳定的产品越适合这种看似“偷懒”的编号方式。因为它把“这个版本是什么时候发布的”这个信息直接写死了不需要你额外去翻 Release Notes。对比一下就清楚了。传统语义化版本号 2.3.1 能告诉你什么它能告诉你这是第二个主版本、第三个次版本、第一个补丁版本但这个版本是上个月发布的还是去年发布的你得去查仓库标签。而 2021.10.25 这个版本号本身就在说话这是 2021 年 10 月 25 日发布的。日期本身就是一种语义而且是最不需要记忆成本的语义。1.2 日期版本号真正解决的核心问题我自己的项目切到日期版本号之后最大的体感是三个问题被同时解决了。第一是可读性。任何拿到版本号的人哪怕完全不懂项目背景也能直接看出发布时间。这对客服、技术支持、甚至用户自己都极其友好。用户反馈“我用的版本是 2021.10.25 那个版本”你不用查任何系统脑子里就能浮现出那个时间窗口。第二是排序性。日期天然单调递增不会出现语义化版本号里 1.9.0 之后跟着 1.10.0 时字符串排序把 1.10.0 排在 1.9.0 前面的那种情况。这个坑太典型了用 YYYY.MM.DD 或者 YYYYMMDD 格式根本不存在这个问题因为年月日的每一位都是定长的字符序就是时间序。第三是溯源。出问题之后版本号直接对应发布窗口能帮你快速锁定那个时间段改了什么。如果项目里还做了版本号到 Git 提交的关联那更能一步到位查到具体代码变更。1.3 它和语义化版本号不是竞争关系有人一听日期版本号就觉得是在否定语义化版本号这其实是个误解。SemVer 和 CalVer 各有各的适用面它们解决的是不同的问题。语义化版本号的核心价值在于对外表达“兼容性承诺”。主版本号变了意味着 Breaking Change次版本号变了意味着向前兼容的新功能补丁号变了意味着修复问题。如果你的项目是个公共库别人要基于它做二次开发那这种精细的表达方式非常必要。日期版本号的核心价值则在于表达“时间真实性”。对于应用软件、内部系统、SaaS 服务用户不关心你有没有破坏性变更用户只关心“我现在用的到底是不是最新版”。这种情况下语义化版本号里的那些约束反而成了负担因为没人会根据你的主版本号来决定要不要升级。实践中还有一种常见的混搭主版本号加日期比如 1.2021.10.25。前面保留主版本号表达重要的兼容性档位后面用日期表达具体发布时机。我自己最后采用的就是这种方案后面会详细说。2. 设计一个日期版本号从格式到含义2.1 主流日期版本号格式对比日期版本号不是只有一种写法不同项目差别还挺大。我整理了几种常见的格式各有各的适用场景。格式示例优点缺点常见场景YYYY.MM.DD2021.10.25可读性最好人类一眼看懂版本号三段略长UI 软件、桌面应用YYYYMMDD20211025适合做文件名、包名不带点可读性差一点固件、镜像、数据快照YY.MM.DD21.10.25短一些跨世纪有歧义移动 App、内部工具主版本.YYYY.MM.DD1.2021.10.25保留主版本档位四位结构初次看到有点怪有兼容性档位需求的软件日期构建号2021.10.25.1同一天可多次发布更长高频发布的团队纯 YYYY.MM.DD 看着清爽但有一个很现实的问题同一天发布了两次怎么办有人会说那还不简单第二次加个 2021.10.25.1 后缀第三次加 .2。这其实就是构建号思路后面我会单独说。2.2 我最终采用的格式和理由我的项目最后定下来的格式是主版本号.年.月.日也就是 1.2021.10.25 这种四段式。加这个主版本号前缀的原因很简单我需要一个表达“重大档位”的地方。举个例子第二版做了数据库不兼容的升级用户必须走迁移脚本。这种变更没法通过日期表达出来因为它不单纯是一个时间概念而是使用方式上的分水岭。于是我把主版本号留在最前面1.2021.10.25 和 2.2021.10.25 之间就是明确的升级分界线。为什么不把时间也加进去做成 1.2021.10.25.15.30没有别的原因就是太长了。版本号最终是给人看、给人读、给人记忆的超过四段之后纯属自找麻烦。如果真需要精确到分钟应该靠构建号或者 Git 提交号去承担而不是一股脑堆进版本号里。2.3 配套的构建号策略构建号是我在实际使用日期版本号时补上的第一块拼图。最初项目用的是纯日期版本号结果某天上午发布了 2021.10.25下午修了一个紧急 Bug 又要发。这时候版本号就尴尬了再发一个一模一样的 2021.10.25那用户根本分不清上午和下午有什么区别。后来我加上了构建号格式变成了 1.2021.10.25.1这个末尾数字每次发版递增。到了第二天日期跳到 2021.10.26构建号自动从 1 重新计数互不干扰。构建号之外还有一个非常有用的补充Git 短提交号。我见过团队把格式定为 1.2021.10.25.ab3f2c1ab3f2c1 就是那个发布版本对应的 Git 提交哈希前七位。这样版本号就不仅仅是“什么时候发布”的问题了它还直接回答了“代码在哪一次提交上”。溯源能力一下子就被拉满了。3. 把 2021-10-25 接进实际发布流程3.1 从 Git 标签到发布产物的全链路光定好版本号格式没用关键是要让版本号真正贯穿整个发布链路。我的做法是让 Git 标签成为唯一的事实来源。打标签的时候直接把版本号作为标签名git tag 1.2021.10.25.1。发布产物的目录名、包名、压缩文件名全部以这个标签命名比如 myapp-1.2021.10.25.1-linux-x64.tar.gz。这样从用户手里的安装包到服务器上的部署目录再到 Git 仓库里的标签整条链路都指向同一个字符串任何环节出问题都可以顺着版本号往回摸。这里有个经验tag 一旦打上就不要轻易改动或删除。版本号应该和时间一样只进不退。有团队成员图方便发现少了东西直接删掉旧标签重新打一个同名标签结果用户手里的版本号和代码对不上排查问题时造成了很大的混乱。3.2 代码里自动生成当天版本号版本号最好由构建系统自动生成而不是人工手动填。手动填的版本号出错的概率太大了尤其是日期这个字段人眼很容易把 10 月 25 日写成 10 月 26 日而且没人会发现。我整理了几个常用场景下的自动生成方式都是我在不同项目里实际用过的。Bash 环境最简单直接用 date 命令# 生成 YYYY.MM.DD 格式 VERSION_DATE$(date %Y.%m.%d) # 生成 YYYYMMDD 格式 VERSION_DATE$(date %Y%m%d) # 如果想带上主版本号前缀 VERSION1.$VERSION_DATE.$BUILD_NUMPython 项目可以用标准库处理from datetime import date version_date date.today().strftime(%Y.%m.%d) version f1.{version_date}.{build_num} print(version)Node.js/JavaScript 项目里可以这样const now new Date(); const y now.getFullYear(); const m String(now.getMonth() 1).padStart(2, 0); const d String(now.getDate()).padStart(2, 0); const version 1.${y}.${m}.${d}.${buildNum};Go 项目因为标准库的日期格式比较特殊写法上有自己的特点package main import ( fmt time ) func main() { versionDate : time.Now().Format(2006.01.02) version : fmt.Sprintf(1.%s.%d, versionDate, buildNum) fmt.Println(version) }如果你用的是 GitHub Actions可以直接在 workflow 里用表达式取日期env: VERSION_DATE: ${{ steps.date.outputs.date }} steps: - name: Set build date id: date run: echo date$(date %Y.%m.%d) $GITHUB_OUTPUT有一点必须强调上述命令有个大前提就是统一时区。这个问题我在第四部分专门讲但在这可以先记住一句话——日期版本号里的“日期”应该是发布时的 UTC 日期或者团队约定的统一时区日期而不是开发者本机时间。3.3 一个版本号追回一次提交日期版本号带来的最大便利体现在线上问题排查的时候。我讲一个真实发生过的场景。某个用户反馈说他用的版本是 1.2021.10.25一直崩溃。第一反应就是先定位这个版本的代码。因为版本号直接对应 Git 标签我执行了 git checkout 1.2021.10.25 切过去再用 git log --oneline 查看这个版本包含的所有提交很快发现里面夹了一个有问题的提交是发布前最后时刻合进去的一个改动。再对比 1.2021.10.24 和 1.2021.10.25 这两个标签之间的差异问题一目了然。如果是语义化版本号这个过程不是不行但会多一个步骤先要去 Release 页面看这个版本对应的发布日期再拿日期区推测 Git 标签。日期版本号把这一步直接省掉了分支上打着的标签本身就带着完整的时间信息。我还见过一个更精细的做法发布之后自动生成一个 manifest 文件里面记录版本号、Git 提交号、构建时间、构建机信息、依赖清单随发布物一起归档。版本号是索引manifest 是详情。有了这两样任何一次发布都能完整复原当时的构建环境排查问题的时候基本是满级装备。4. 我踩过的坑和排查技巧4.1 同一天多次发版构建号是刚需前面说到我最初用纯日期版本号踩过的最大一个坑就是同一天发多个版本。第一次遇到是某个版本有紧急安全修复上午刚发了 2021.10.25下午又要发补丁。当时完全没有构建号机制我纠结了半天怎么命名最后不得不临时定了 2021.10.25-hotfix 这样一个不伦不类的名字。从此之后我把构建号硬性规定为版本号的必备字段。团队的规矩是一年内构建号不要清零跨天自动归零。因为每天发版频率不会高到超过 99 次两位足够用了。这里要注意的是构建号必须由 CI 系统保证递增不能靠开发者本地拍脑袋否则一到并发发布就会冲突。4.2 时区问题让版本号“穿越”了时区问题是我在实际使用中踩过最隐蔽的坑之一。团队里有同事在晚上十一点半发布了版本他本机显示的日期是 2021.10.25但构建服务器用的时区已经跨到了 2021.10.26。于是出现了产品里显示的版本号是 2021.10.25而 Git 标签上的日期是 2021.10.26两边对不上。后来统一规则无论开发者身在哪个时区版本号一律按照构建系统所在时区的日期生成。更严格一点的话可以统一用 UTC 日期。但这里有个权衡如果是一个面向国内用户的内部系统UTC 日期经常和白天的发布日期差一天看起来反而别扭。所以最终团队的约定是版本号里的日期取 CI 服务器的本地日期CI 服务器时区固定为东八区所有人都以它为准。4.3 预发布版本怎么命名日期版本号加上预发布标识也是很多团队会犹豫的地方。beta、rc 这些后缀该往哪放我的做法是放在日期之后、构建号之前。比如 1.2021.10.25.rc1正式发布是 1.2021.10.25.1。这样预发布版本和正式版本在时间上对齐但通过后缀明确区分。有一个细节要注意预发布版本的分发渠道必须和正式版本隔离。我遇到过团队把 rc 版本直接发到了更新通道结果一批用户被自动升级到了测试版本。排查到最后就是版本号看着太像正式版更新服务基于字符串前缀判断把 1.2021.10.25.rc1 误当成了正式版本。加一道显式的稳定版校验逻辑比单纯靠版本号区分要可靠得多。4.4 常见问题速查表最后把我在日期版本号使用过程中遇到的一些高频问题整理成一个速查表方便你排查时对照参考。问题可能原因解决办法同一天发了两个版本版本号重复没有构建号机制在日期后追加构建号CI 自动递增版本号日期和 Git 标签日期不一致时区不统一固定 CI 服务器时区所有版本号以 CI 生成时间为准版本号排序有问题用了 YY.MM.DD 或 M.D 这种非定长格式改用 YYYY.MM.DD保证字符序即时间序用户被自动升级到测试版预发布版本号和正式版太接近更新服务误判预发布版本走独立渠道更新逻辑校验稳定版标识版本号对不上线上代码手动填版本号填错彻底改为构建系统自动生成版本号禁止手工填写发布后无法定位对应提交未建立版本号与 Git commit 的关联使用“版本号短提交号”绑定或生成 manifest 归档文件某个旧版本需要重新发布tag 被你删掉重建内容已改变永远不要删 tag需要修旧版报一个递增的构建号即可跨天后的凌晨发版昨天的构建号被沿用日期变化时构建号从 1 重新开始我个人在实际操作中最深的体会是日期版本号不是一个“看着简单就随便用”的东西它的下限很低上限也很低但中间的细节很容易被忽略。格式选错、时区不理清、构建号不设计照样会把发布流程搅成一锅粥。但只要你把它当成一套完整的版本管理体系来对待它的回报是相当大的——当你拿着一串 1.2021.10.25 就能在三分钟内定位到线上问题对应的那一次提交时你会觉得当初在这个日期字符串上花的心思都是值得的。最后再分享一个小技巧。我在 CI 脚本里始终保存一个当前版本的“现场快照”包括版本号、构建时间、构建机 IP、Git 分支和提交号发布成功后自动推送到内部归档站点。这个动作看似不起眼却在后来处理“用户说用了旧版本但系统显示更新成功”这类扯皮问题上帮了大忙。版本管理这事很多时候不是技术难点而是“当时没留证据”。日期版本号加上归档快照至少能把证据这块补得严严实实。