1. 先把“发行版交付”这件事说清楚
三个月前帮一个团队收拾烂摊子,事情不复杂:他们把压缩包发给了客户,客户解压之后发现里面躺着半年前的老代码,还有一个不该出现的测试配置文件。仓库里的代码是对的,交付出去的东西是错的,中间那段“从提交到交付物”的过程,全靠某个人在自己电脑上手动复制粘贴。这就是典型的把 Git 当成网盘用,而不是当成发布链路的一环。发行版交付(Release Delivery)在 Git 语境下指的正是这一段:把仓库里某个确定的提交,变成一份可识别、可校验、可复现、可分发的发行物。它解决的从来不是“代码写没写完”,而是“我发出去的那一份,到底是哪一份”。这套东西适合所有需要把软件或内容交出去的人,无论你交付的是一个后端服务、一个前端包、一份配置模板,还是一套文档站,只要你曾经在微信里发过 zip,这篇就值得往下看。
很多人以为交付就是打个 tag 然后点一下发布按钮,真到出事那天才发现,tag 打在哪个提交上、包里有没有脏文件、别人拿到能不能核对、出问题能不能回溯,这四个问题一个都答不上来。十七篇写到这一节,说明前面的分支、合并、冲突处理都已经铺完了,现在是收获环节,也是最容易翻车的环节。我下面把这套流程拆成设计考量、执行细节、故障排查、替代方案四块,命令都是能直接抄的,理由也都会讲清楚,你按自己团队的规模裁剪就行。
1.1 Git 在交付链路里究竟承担了哪一段
把交付想象成寄快递:Git 负责的是“把货物封箱、贴上一张全球唯一且不可伪造的单号”,至于用什么快递公司、路上怎么走,那是构建系统和分发渠道的事。这个分工必须拎清楚,否则就会出现“用 Git 当构建工具”的怪现象,比如有人在仓库里提交编译产物,或者靠分支名来区分线上版本,最后仓库膨胀到几个 G,克隆一次要半小时。Git 提供的能力其实只有三样:第一是内容寻址,每个提交有一个由内容算出来的哈希,改了任何字符都会变;第二是引用,标签和分支都是指向提交的可读名字;第三是归档导出,能把某个引用的树对象导出成不含版本库元数据的干净文件。这三样组合起来,就构成了“可识别 + 可复现”的最小交付基础。
反过来讲,Git 不擅长也不该承担的事情包括:依赖安装、编译打包、产物签名以外的证书管理、增量更新分发、灰度发布。我见过一个项目把整个 node_modules 提交进仓库,理由是“这样交付最保险”,结果仓库大到托管平台直接拒收。正确的姿势是仓库里只放源码和构建描述文件,交付时由构建环节产出产物,Git 只负责给源码那一段打上准确的锚点。这个边界一旦立住,后面的所有流程都会顺很多,因为你知道该信任什么、该核对什么。
还有一层容易被忽略:交付的对象往往不止源码。一个完整的发行版通常包含源码快照、变更说明、校验和文件、可选的构建产物、以及一份说明“这一版对应哪个提交”的元数据。Git 能直接产出的是源码快照和提交元数据,变更说明可以靠日志生成,校验和靠外部命令算,构建产物则要由构建系统负责。把这些东西列成清单再动手,比上来就敲命令靠谱得多。
1.2 使用要求:交付前仓库必须满足的条件
发行版交付对仓库状态有硬性要求,不满足就别发,发了就是给自己埋雷。我把这些年总结的准入条件列成表格,你可以直接拿去当检查项用。
| 检查项 | 合格标准 | 不合格的后果 |
|---|---|---|
| 工作区状态 | git status输出干净,无未跟踪文件 | 交付包可能漏文件,或误带临时文件 |
| 目标提交位置 | 位于主干或约定的发布分支上 | 交付了一个游离在主线之外的提交 |
| 标签唯一性 | 目标版本号在本地和远端都不存在 | 标签被覆盖,历史锚点被篡改 |
| 提交可追溯 | 能从标签回溯到完整父链 | 出问题无法定位引入点 |
| 构建可复现 | 同一提交在同一环境下产物一致 | 客户拿到的和你测的不是一份 |
| 元数据完整 | 版本号、提交号、构建时间可查 | 事后无法确认客户装的是哪版 |
这里最容易糊弄的是第三条。git tag默认允许重名覆盖吗?不允许,同名标签会直接报错,但加上-f就能强推,远端也一样,普通推送会被拒绝,加--force就能覆盖。也就是说标签的不可变性是靠约定和权限守住的,不是天然铁律。所以我在团队里坚持一条规矩:标签一旦推送到远端,任何人不得删除或移动,需要修正就发新版本号。这条规矩听着死板,但它省下的排查时间远超你想象。
另一个常被跳过的条件是“工作区干净”。有人觉得反正git archive只导出已提交内容,工作区脏不脏无所谓。理论上没错,但实践里工作区脏往往意味着“还有改动没提交”,你打标签的时候很可能打在了缺少关键修复的提交上。所以我的习惯是先git status看一眼,有改动就问一句“这些是不是该进这一版”,问清楚了再动手。
1.3 交付物的构成与命名规范
命名这件事在交付里权重很高,因为它是人跟版本之间的第一道索引。我见过太多final.zip、final_v2.zip、final_v2_真final.zip,这种命名等于没有命名。建议采用项目名-版本号的固定格式,版本号用语义化版本三段式,需要预发布标识就加后缀,例如某服务-1.4.0.tar.gz、某服务-1.5.0-rc.1.tar.gz。压缩包内部的顶层目录名要和包名一致,解压出来不会散落一地文件,这一点git archive的--prefix参数正好能解决,后面会给具体写法。
一份完整的交付物我一般这么组织:
- 源码快照:由
git archive从标签导出,不含.git目录,不含被export-ignore排除的文件。 - 变更说明:从上一个标签到当前标签之间的提交摘要,按类型归类。
- 校验和文件:对源码快照和构建产物分别计算 SHA-256,汇总成一个文本文件。
- 版本元数据:一个几十字节的小文件,写明版本号、完整提交哈希、构建时间、构建环境标识。
- 可选构建产物:由构建系统从同一提交产出,与源码快照并列存放。
这套东西加起来通常不到几 MB,但它在出问题时的价值极高。客户说“装不上”,你让他把版本元数据文件发过来,一眼就知道他装的是哪一版、对应哪个提交,不用再玩“你截个图我看看”的游戏。这个习惯我是被逼出来的,早期有一次客户报了个诡异问题,查了两天才发现他装的是半年前的包,白白耗掉两个工作日。
2. 版本锚点的选型:为什么用标签而不是分支
这是整个交付设计里最核心的一个决策,值得单独拿出来讲清楚。新手常见的做法是开一个release分支,发版时把代码合并进去,交付就从分支导。这个做法在只有一两个人的时候勉强能用,人一多必然出问题,原因是分支是可变的。你今天从release分支导出 1.4.0,明天有人往这个分支上合了一个修复,分支指向就变了,后天你想重新导出一份“1.4.0”给另一个客户,导出来的内容和第一天不一样了。同一版本号对应两份不同内容,这在交付里是灾难级别的错误,因为它摧毁了“版本号”这个概念本身的意义。
标签的语义完全不同:它是钉死在某个提交上的引用,除了人为强制移动,否则永远指向那一个提交。用标签做锚点,你才有资格说“1.4.0 就是这一份,谁来都是这一份”。分支继续承担它的本职工作——让开发在上面滚动前进,两者互不干扰。我通常的做法是发版时在主干上打标签,同时如果确实需要维护线,就从标签拉一个维护分支出来,命名成1.4.x之类,专门用来接补丁。这样锚点是标签,通道是分支,职责分明。
2.1 轻量标签、附注标签与签名标签的区别
三种标签在命令上只差一个参数,在交付里的分量完全不同,选错了会在审计和校验环节吃亏。
轻量标签就是git tag v1.4.0,本质上是往refs/tags/v1.4.0写一个指向提交的引用,不产生额外对象。它轻,但没有作者、没有时间、没有说明,git show出来只有提交信息。用在临时标记上没问题,用在正式发版上就显得太随意了。
附注标签是git tag -a v1.4.0 -m "...",会在对象库里创建一个独立的标签对象,里面记录了打标签的人、时间、说明文字,以及指向的提交。它是完整的一等公民,能被git describe正确识别,能被git tag -n列出说明,能作为交付锚点的正式载体。我的建议是正式发行版一律用附注标签,多敲两个字符换来的是一份可审计的记录。
签名标签是git tag -s v1.4.0 -m "...",在附注标签基础上用自己的密钥对标签对象签名。它在需要证明“这个版本确实出自我们”的场景下才有必要,代价是团队每个人都要有一套可用的密钥和分发公钥的流程。中小团队不一定需要走到这一步,但如果你的交付对象会做来源核验,这一步能省掉很多扯皮。
# 轻量标签:仅适合临时标记 git tag v1.4.0-tmp # 附注标签:正式发行版的默认选择 git tag -a v1.4.0 -m "release: 1.4.0 首个稳定版,包含配置加载重构" # 签名标签:需要来源证明时使用 git tag -s v1.4.0 -m "release: 1.4.0" # 验证签名 git verify-tag v1.4.0 # 查看标签对象完整信息(附注标签和签名标签才有) git cat-file -p v1.4.02.2 版本号与标签命名的约定
版本号不是随便编的,它是一份对外承诺。语义化版本的规则很朴素:主版本号变动表示不兼容的改动,次版本号变动表示向下兼容的新增,修订号变动表示向下兼容的修复。这条规则的价值在于,使用方看到版本号就能判断升级风险,不用读你的变更说明。我见过把内部重构也升主版本号的项目,结果使用方对每次升级都如临大敌,最后干脆锁死不升,反而失去了发布的意义。
标签命名上,我建议统一加v前缀,即v1.4.0,这与绝大多数工具链的默认约定一致,git describe的输出也更自然。预发布版本用连字符后缀,例如v1.5.0-rc.1、v1.5.0-beta.2,这类标签不应该被当成正式发行版处理,导出和分发时要区别对待。内部试用的构建可以用构建号做后缀,例如v1.4.0+build.20240612,加号后面的部分按语义化版本规范不参与优先级比较,正好适合放这种临时信息。
还有个小坑:标签名不要和分支名撞车。Git 允许refs/tags/x和refs/heads/x同时存在,但你在命令行里写git checkout x的时候会收到歧义警告,某些脚本里这种歧义会直接导致取错对象。约定上让分支用feature/xxx、release/1.4.x这类带斜杠的名字,标签用纯版本号,天然就错开了。
2.3 标签与提交的绑定关系及不可变性边界
标签指向的是提交对象,这一点在附注标签上要绕一层:refs/tags/v1.4.0指向标签对象,标签对象里有个object字段指向提交。所以核对的时候直接git rev-parse v1.4.0得到的是标签对象的哈希,要拿提交哈希得用git rev-parse v1.4.0^{commit}。这个细节在写自动化脚本时特别重要,我见过脚本里直接用标签名去比对提交号,结果永远对不上,排查了半天才发现是解引用层级的问题。
不可变性的边界前面提过,这里再强调一次具体的防护手段。远端仓库层面,可以让服务端拒绝非快进的引用更新,标签的覆盖推送自然会被挡住;权限层面,把打标签和推标签的权限收拢到发布负责人手上,普通开发者只能读;流程层面,在交付检查清单里写明“标签只增不改”。三道防线里流程最弱但最有效,因为工具总有人能绕过,规矩立住了大家反而不纠结。
真要修正一个已发布的标签怎么办?答案是不要修正,发新版本。哪怕是说明文字写错了这种小事,也发v1.4.1,然后在变更说明里写一句“修正 1.4.0 版本说明中的笔误”。听起来很浪费,但这是唯一能保住“版本号与内容一一对应”这个契约的做法。我吃过一次亏:为了改一句说明重新推了同名标签,结果两个客户手里的包内容一致但标签对象不同,对账的时候谁也说不清哪个是原始发布,最后只能全部重发。
3. 完整交付流程的落地实现
下面走一遍完整流程,从体检到产出可交付文件。假设当前版本是v1.3.2,这次要发v1.4.0,主干分支叫main。命令都是实际能跑的,路径和项目名换成你自己的即可。
3.1 第一步:交付前的仓库体检
体检的目的是确认“我即将打标签的这个提交,是我想要的那个提交”。很多人跳过这步直接打标签,事后发现打在了本地未推送的提交上,远端根本没有这个对象,推标签的时候才报错,白折腾一轮。
# 1. 确认工作区干净 git status --short # 2. 同步远端并确认本地主干与远端一致 git fetch origin git log --oneline -1 origin/main git log --oneline -1 main # 3. 确认目标版本号在本地和远端都不存在 git tag -l "v1.4.0" git ls-remote --tags origin "refs/tags/v1.4.0" # 4. 看一下从上一个版本到现在的提交范围是否合理 git log --oneline --no-merges v1.3.2..main第三步的两个命令必须都执行。只查本地会漏掉“别人已经推过同名标签”的情况,只查远端会漏掉“本地有残留标签”的情况。第四步的输出值得逐条扫一眼,主要看有没有不该进来的提交,比如某个实验性功能的分支被误合进来,或者某个提交信息里写着“临时提交,稍后修正”。这一步花三分钟,能省掉后面三小时。
体检还有一项容易被忽略:构建验证。理想情况下,CI 已经在这个提交上跑过完整构建和测试,你只需要确认那次构建的结果是绿的。如果没有 CI,那至少要在本地从这个提交做一次干净构建,别用带着缓存的开发环境,缓存会让你误以为没问题。
3.2 第二步:打标签并核对指向
# 打附注标签 git tag -a v1.4.0 -m "release: 1.4.0 主要变更: - 重构配置加载逻辑,支持多环境覆盖 - 修复高并发下的连接泄漏 - 新增导出接口的超时参数" # 核对:标签指向的提交是否是预期提交 git rev-parse v1.4.0^{commit} git rev-parse main # 查看标签详情 git show v1.4.0 --stat --no-patch打完标签第一件事就是核对提交哈希,两个输出一致才算过。我习惯把这一步做成肌肉记忆,因为打错标签的原因太多了:切到了错误的分支、本地落后远端几个提交、在游离 HEAD 状态下操作。核对只要两秒,不核对的代价是重发整个版本。
标签说明里写变更摘要这件事,看起来是锦上添花,实际上是给未来的自己留线索。半年后你或者别人查git tag -n的时候,能直接看到这版干了什么,不用去翻提交日志。写的时候注意别罗列一堆提交标题,提炼成三到五条人话即可。
3.3 第三步:推送标签并处理推送被拒
# 只推这一个标签,比 --tags 安全 git push origin v1.4.0 # 若使用附注标签且想连带提交一起推,可以用 git push --follow-tags origin main # 查看远端标签确认已到位 git ls-remote --tags origin | grep v1.4.0这里要讲清楚git push origin v1.4.0和git push --tags的区别。后者会把本地所有标签全推上去,包括你本地随手打的临时标签、实验标签,这些脏东西一旦到了远端就很难清理,还得让别人去删。所以永远推具体标签,别图省事。
推送被拒最常见的原因是远端已存在同名标签,报错信息类似! [rejected] v1.4.0 (already exists)。这时候不要加--force,先查清楚远端那个标签是什么时候、被谁、指向哪个提交推上去的,用git ls-remote --tags origin拿到哈希,再比对本地是否有这个对象。如果确认是误推的空标签或者测试标签,删除远端标签也要走一遍确认流程,别一个人闷头删。合并冲突可以解决,标签冲突没法解决,只能靠沟通。
3.4 第四步:用 git archive 生成发布包
git archive是交付环节的主力工具,它能把某个引用的树导出成 tar 或 zip,且不带.git目录、不带未跟踪文件、不带工作区改动,天然适合做发行包。
# 生成 tar.gz,顶层目录带版本号前缀 git archive \ --format=tar.gz \ --prefix=某服务-1.4.0/ \ -o dist/某服务-1.4.0.tar.gz \ v1.4.0 # 生成 zip(给不熟悉 tar 的使用方) git archive \ --format=zip \ --prefix=某服务-1.4.0/ \ -o dist/某服务-1.4.0.zip \ v1.4.0 # 校验包内容,确认没有多余文件 tar -tzf dist/某服务-1.4.0.tar.gz | head -30--prefix那个结尾斜杠千万不能省。省了之后生成的包解压出来顶层目录名会粘连成某服务-1.4.0README.md这种鬼东西,我第一次用的时候就踩了这个坑,客户解压完发消息问我是不是包坏了。加上斜杠,所有文件都会规规矩矩待在某服务-1.4.0/目录下。
git archive有两个高级用法值得掌握。第一个是export-ignore,在仓库根目录的.gitattributes里给不想进包的文件打上这个属性,导出时就会被剔除。测试用例、CI 配置、编辑器配置、贡献指南这些通常不需要随发行版发出去。
# .gitattributes .github/ export-ignore tests/ export-ignore .editorconfig export-ignore CONTRIBUTING.md export-ignore第二个是export-subst,配合文件里的占位符使用,导出时会把占位符替换成实际的提交信息。这在做“版本元数据文件”时特别好用,不用额外写脚本注入。
# .gitattributes VERSION.txt export-subst# VERSION.txt version=$Format:%(describe:tags)$ commit=$Format:%H$ commit_short=$Format:%h$ commit_date=$Format:%cI$导出之后VERSION.txt里的占位符会被替换成实际值,客户拿到包一看就知道对应哪个提交。这个机制的好处是它作用于导出环节而非仓库内容,仓库里始终是占位符,不会因为每次发版都产生一个“更新版本文件”的提交。
需要提醒的是git archive的三个限制:不递归子模块,如果你的仓库用了子模块,导出包里对应目录是空的,得单独处理;不含未跟踪文件,任何靠.gitignore排除但实际需要的文件都会丢;不含 Git LFS 的实际内容,LFS 管理的文件导出来是指针文本而不是真文件。这三条我都在真实项目里遇到过,尤其第三条最隐蔽,因为包能正常解开,只是大文件变成了几十字节的文本,等到使用方加载失败才发现。
3.5 第五步:生成变更说明与校验和
# 变更说明:按提交类型分组,去掉合并提交 git log --no-merges \ --pretty=format:"- %s (%h)" \ v1.3.2..v1.4.0 > dist/CHANGELOG-1.4.0.md # 贡献者统计(可选,用于发布公告) git shortlog -sne v1.3.2..v1.4.0 # 代码变更规模 git diff --stat v1.3.2 v1.4.0 # 计算校验和 cd dist sha256sum 某服务-1.4.0.tar.gz 某服务-1.4.0.zip > SHA256SUMS cat SHA256SUMS # 使用方核验方式 sha256sum -c SHA256SUMS变更说明的生成只是起点,直接丢一堆提交标题给使用方看并不友好,我通常会在生成的基础上手工归类成“新增”“修复”“破坏性变更”三块,破坏性变更放最前面并加粗。这个动作花十分钟,能挡掉大量“升级之后怎么不兼容了”的咨询。
校验和这块,很多团队觉得没必要,理由是“我们又不做安全审计”。这个理解偏了,校验和解决的首要问题不是防篡改,而是防传输损坏和防版本混淆。文件经过邮件、网盘、聊天工具转发几次,损坏的概率不低,有了校验和,使用方一条命令就能确认自己拿到的是完整的。第二个作用更实际:客户手里有三个包不知道哪个是哪个,算一遍哈希对上号,比翻邮件记录快多了。
4. 常见故障与排查实录
交付环节的问题有个共同特征:症状出现在使用方那边,根因在你这边的操作。所以排查思路永远是先在本地复现使用方的动作,把“拿到包之后做什么”完整走一遍。
4.1 标签类故障速查
| 现象 | 可能原因 | 排查命令 | 处理方式 |
|---|---|---|---|
| 推标签报 already exists | 远端已有同名标签 | git ls-remote --tags origin | 核对后决定是否另发版本号 |
git describe取不到版本 | 只打了轻量标签或标签未拉取 | git fetch --tags | 补拉标签,正式版改用附注标签 |
| 标签指向的提交不对 | 打标签时不在目标分支 | git rev-parse v1.4.0^{commit} | 删除本地标签重打,未推送才行 |
| 别人拉不到标签 | 只推了分支没推标签 | git ls-remote --tags | 单独推送该标签 |
| 检出标签后无法提交 | 处于游离 HEAD 状态 | git status | 需要改代码就从标签拉分支 |
“检出标签后无法提交”这条值得多说两句。新手经常git checkout v1.4.0然后直接改代码,改完发现提交不在任何分支上,切走就找不到了。正确做法是git switch -c hotfix/1.4.1 v1.4.0,基于标签开一个修复分支,改完再合并、再打新标签。这个动作我在代码评审里纠正过很多次。
4.2 发布包内容类故障
包里少了文件,第一反应是查.gitignore和.gitattributes。前者会让文件压根没进仓库,后者如果配了export-ignore会让文件不进包。两个都排查完还没找到,就看是不是子模块或者 LFS 的问题,前面提过。
包里多了文件,通常是打包时用了git archive之外的命令,比如直接tar -czf整个工作目录,把.git、node_modules、本地配置全带上了。这种情况我建议直接换回git archive,别试图用排除参数修补,排除列表只会越来越长。
包解开后权限不对,可执行脚本丢了执行位。git archive会保留仓库里记录的文件模式,所以问题往往出在仓库本身:文件在提交时就没有执行位。用git ls-files -s查看模式,100644是普通文件,100755是可执行文件,发现不对就git update-index --chmod=+x 脚本路径修正后重新提交。这个坑在跨系统协作时特别常见,某同学在另一个系统上提交了脚本,执行位天然丢失。
包大小异常,先看是不是把大文件历史带进来了。git archive只导出目标提交的树,不含历史,所以包大通常意味着当前提交里就有大文件。用git ls-tree -r -l v1.4.0 | sort -k4 -n -r | head -20按大小排序看一眼就清楚了。
4.3 交付后发现缺陷的补救路径
这是最考验流程设计的一环。假设v1.4.0发出去三天,发现一个必须修的缺陷。有两条路,选哪条取决于主干现在是什么状态。
第一条路,主干可以立刻发新版。那就从主干修,修完测试通过,打v1.4.1,走一遍完整交付流程,然后在公告里说明 1.4.0 存在的问题和 1.4.1 的修复内容。这是最干净的做法,历史是线性的,没有分叉。
第二条路,主干已经合入了大量未完成的新功能,不能直接发。那就基于v1.4.0标签拉维护分支,在分支上修,修完打v1.4.1,同时把这个修复也合并回主干,避免下次发版时缺陷复现。
# 基于标签开维护分支 git switch -c release/1.4.x v1.4.0 # 修复并提交 git commit -am "fix: 修复导出接口超时参数失效" # 打补丁版本标签 git tag -a v1.4.1 -m "release: 1.4.1 修复导出接口超时参数失效" git push origin release/1.4.x v1.4.1 # 别忘了回合到主干 git switch main git cherry-pick <修复提交>最后那个cherry-pick是最容易漏的一步。我见过一个项目连续发了三个补丁版本,每次都在维护分支上修,从来没回合主干,等主干下一次发版的时候,那三个修复全丢了,用户直接遭遇回归。所以在维护分支上修 bug 的时候,心里要默念一句“这个改动主干也要”。团队层面可以在交付清单里加一条“补丁修复是否已回合主干”,勾选确认再发。
5. 替代解决方案与选型建议
发行版交付不是只有标签加归档一条路,不同规模、不同交付对象适合的方案不一样。下面几种我都实际用过,讲清楚各自适合什么场景。
5.1 分支冻结式交付
做法是发版前拉一个release/1.4分支,冻结功能只接修复,测试稳定后从这个分支导出交付。它的好处是可以在分支上继续打补丁而不用动主干,适合那种“发版后必然有一堆小问题要修”的项目。缺点是锚点仍然是分支,如果不在导出时同时打标签,就会出现“同一个版本号导出两次内容不同”的问题。
我的用法是两者结合:拉维护分支做修复,每次导出前在分支上打标签,以标签为准导出。这样分支负责承载修复工作流,标签负责锁死交付内容,各司其职。单纯用分支不用标签的方案,我建议只在小规模内部项目上用,对外交付还是老老实实打标签。
5.2 构建产物交付与源码归档的对比
有些场景交付的不是源码,而是编译好的产物,比如给客户一个可执行程序或者一个前端静态包。这时候git archive就不合适了,得由构建系统从标签对应的提交构建出产物,再对产物做校验和与分发。关键是构建过程必须绑定标签,也就是构建脚本里要能取到当前构建对应的标签名和提交哈希,把它写进产物的元数据里。
# 构建脚本里注入版本信息(以某构建工具为例) VERSION=$(git describe --tags --always --dirty) COMMIT=$(git rev-parse --short HEAD) BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ) # 通过环境变量或参数传给构建流程 export APP_VERSION="$VERSION" export APP_COMMIT="$COMMIT" export APP_BUILD_TIME="$BUILD_TIME"--dirty这个参数很关键,它会在工作区有未提交改动时给版本号加上-dirty后缀。正式构建时如果看到这个后缀,说明有人在非干净状态下构建,产物不可复现,应该直接判定构建失败。这个小机制挡住过我们好几次“本地改了配置忘了提交就打正式包”的事故。
源码归档和产物交付的区别在于可复现性责任方不同。源码归档把复现责任交给使用方,你只需要保证源码和构建描述完整;产物交付把复现责任揽在自己身上,你必须保证构建环境可重现。后者的成本高得多,只有在使用方确实没有构建能力时才值得。
5.3 git bundle 离线交付
有些交付环境没有网络,连仓库都拉不下来。这时候可以用git bundle把仓库的一部分打包成单个文件,对方拿到之后能从 bundle 里克隆出完整仓库,包括历史。
# 打包全部历史与标签 git bundle create 某服务-1.4.0.bundle --all # 只打包某个范围(体积更小) git bundle create 某服务-1.4.0.bundle v1.3.2..v1.4.0 --tags # 对方验证并克隆 git bundle verify 某服务-1.4.0.bundle git clone 某服务-1.4.0.bundle 某服务bundle 的好处是它保留了完整的 Git 语义,对方拿到之后可以正常查日志、切分支、看 diff,而不是面对一堆散文件。缺点是体积比源码归档大,且对方需要会用 Git。适合交付对象也是开发团队、需要看历史的场景。
5.4 几种方案怎么选
| 方案 | 锚点 | 体积 | 可复现性 | 适用场景 |
|---|---|---|---|---|
| 标签 + archive | 标签 | 小 | 高 | 对外交付源码,主流选择 |
| 分支冻结导出 | 分支 | 小 | 低(易漂移) | 内部快速迭代,短期使用 |
| 标签 + 构建产物 | 标签 | 大 | 取决于构建环境 | 使用方无构建能力 |
| git bundle | 标签范围 | 中 | 高 | 离线环境,交付给开发团队 |
选型的原则很简单:只要交付对象是外部方,一律用标签做锚点。内部项目可以松一点,但也别松到没有锚点。我见过最离谱的情况是整个团队靠“某台机器上的某个目录”当发布源,那台机器一重装,所有历史版本就都没了。
6. 我的交付清单与踩坑记录
6.1 一页纸交付检查清单
这张清单我贴在显示器边上很多年,每次发版照着走一遍,五分钟能过完,省下的事故不计其数。
- 工作区干净,
git status --short无输出 - 本地主干与远端一致,
git fetch后无差异 - 目标版本号本地、远端均不存在
- 待发布提交已通过构建与测试
- 附注标签已打,说明文字写清核心变更
- 标签指向的提交哈希已核对
- 标签已推送,远端可见
- 发布包已用
git archive生成,--prefix带结尾斜杠 - 包内容已抽查,无
.git、无临时文件、无缺失文件 - 校验和已计算并随包发布
- 变更说明已整理,破坏性变更置顶
- 若为补丁版本,修复是否已回合主干
清单本身不复杂,价值在于它把“靠记忆”变成“靠流程”。人在赶时间的时候最容易跳步,而跳的那一步往往就是出事的那一步。
6.2 几个印象深刻的坑
第一个坑是标签名带空格。早期有一次手滑写了git tag -a "v1.4.0 正式版",本地看着没事,推上去之后各种脚本全挂,因为很多工具在解析引用名时不处理空格。标签名只用字母、数字、点、连字符、下划线,其他字符一律不要。说明文字放-m里,那里随便写。
第二个坑是导出时忘了切到标签对应的提交。有人习惯先git archive再核对,结果导出的是当前工作区所在分支的头,而标签打在另一个提交上。正确顺序永远是先确认标签指向,再git archive <标签名>,把标签名显式写在命令里,永远不要省略成当前 HEAD。
第三个坑是压缩包内的时间戳。git archive生成的文件时间戳取自提交时间,这是好事,同一标签导出的包内容完全一致,包括时间戳。但如果有人在导出后再用其他工具重新压缩一遍,时间戳就变成了当前时间,包的哈希每次都不同,校验和对不上。所以包生成之后不要再二次处理,需要改包名就改文件名,不要解压重压。
第四个坑是发布说明里的链接失效。变更说明里引用了提交链接,指向的是内部代码托管地址,使用方根本打不开。对外发布的说明里,链接要么指向公开的变更记录,要么干脆去掉,只留提交短哈希和标题。
第五个坑最隐蔽:默认分支改名之后的老标签。仓库默认分支从旧名字改成新名字,旧标签不受影响,但某些自动化脚本里硬编码了旧分支名去取内容,改了名之后脚本静默失败,导出的包内容为空。这个问题的教训是脚本里不要硬编码分支名,能通过标签或提交哈希定位就绝不用分支名。
这套流程跑顺之后,一次正式交付从体检到产出所有文件,熟练的话十分钟以内能完成。真正花时间的从来不是敲命令,而是发版前那些“这个改动到底该不该进这一版”的判断。工具能帮你把内容锁死、把校验做足,但“发什么”这件事,还是得靠人对项目的理解。我现在养成的习惯是发版前一天把待发内容过一遍,把有疑问的提交挂起来单独问,而不是等到发版当天边打标签边纠结——那个状态下做的判断,往往事后都要返工。