发布事故几乎都不是发布失败,而是要撤回时发现撤不回去:版本号随手写的、回滚点没记、迁移不能反向。
不知道版本号定多少,按版本号由提交类型推导算;线上要撤回,直接照回滚步骤那六步做。
现在立刻回滚,版本号、镜像、数据库停在哪一步,这三样是不是都写得出来?
答「否」,说明这次发布没有回滚点。缺的是发布说明里的「回滚到」那一行和迁移的反向步骤,补上再发。
版本号不靠人拍,它由这一段区间里的提交类型决定——分支与提交那一页已经规定了类型,所以这一步可以算出来。标签用 v 开头,指向 main 上要发布的那一次提交。
| 这段区间里包含 | 版本号变化 | 例子 |
|---|---|---|
有不兼容变更(脚注里有 BREAKING CHANGE:,或类型后带 !) | 主版本号 +1,其余归零 | v1.4.2 → v2.0.0 |
至少一条 feat,且无不兼容变更 | 次版本号 +1,修订号归零 | v1.4.2 → v1.5.0 |
只有 fix、perf、refactor、docs、test、chore | 修订号 +1 | v1.4.2 → v1.4.3 |
课程项目处在 0.x 阶段时遇到不兼容变更 | 只把次版本号 +1 | v0.4.2 → v0.5.0 |
| 同一天第二次发布 | 在上一次的基础上继续 +1 | v1.5.0 → v1.5.1 |
第一条命令列出区间内的提交,第二条只挑出决定版本号的那几类。看结果对着上面的表选一个号,再打标签。
git describe --tags --abbrev=0 # 上一版标签,也就是回滚点 git log --oneline --no-merges "$(git describe --tags --abbrev=0)..main" git log --format=%B "$(git describe --tags --abbrev=0)..main" | grep -E '^(feat|fix|perf)(\(|!|:)|BREAKING CHANGE' git switch main && git pull --ff-only git tag -a v1.5.0 -m "feat: 按会话里的所属范围读取笔记" git push origin v1.5.0
0.x 用到什么时候:0.x 只是一个说法,含义是「还没有对外承诺兼容性」。第一次把服务交给这个小组之外的人使用之后,就升到 1.0.0,从那时起不兼容变更必须走主版本号。语义化版本的准确定义与 0.x 的处理以官方规范为准。
顺序固定为:本地检查通过、测试环境、预发、生产。测试环境用测试数据;预发的配置接近生产,仍不使用生产库的写入口。清单里每一行都要给出命令或数值,不写「确认没问题」。
make check,它已经包含目录白名单、迁移命名、文档同步与静态检查。go test ./... -race 或 pytest -q,退出码 0。make migrate-roundtrip,只对测试库跑。git describe --tags --abbrev=0 给出上一版标签;那一版要用的镜像或构建产物还能拉到。v1.5.0,回滚到 v1.4.2。gitleaks detect --no-banner --redact,命中数 0。发布之后核对的判据同样是数值。没有基线就没法判断「新的错误」,所以发布前先记下当前的错误条数。
GET /healthz 200、GET /notes 200、跨所属范围 GET /notes/{id} 404。命令:curl -s -o /dev/null -w '%{http_code}\n' http://localhost:${WEB_PORT:-8080}/healthz。docker compose logs --since 10m api | grep -c '"level":"ERROR"',与发布前同一时间窗的数字对照。docker compose logs --since 10m api | grep -c 'cross-scope access denied' 应大于 0(在做过越权核对之后),字段见日志。没有预发环境时:预发环境在课程项目里常常不存在,此时可以合并为「测试环境 + 生产」两段,但必须在发布说明里写明少了哪一段,不能默认跳过。灰度、蓝绿、金丝雀留到仓库里有对应流水线的那次改动,本次先把顺序、回滚点和核对清单写下来。
回滚条件要事先定好,否则会变成现场争论。下面三条是阈值:任一条命中就回滚,不回滚也要在发布说明里写明理由。回滚不是失败,回滚时才开始讨论才是。
level":"ERROR" 的条数达到发布前的两倍。cross-scope access denied,说明记录链路断了。git switch --detach v1.4.2 && docker compose up --build -d,或直接拉上一版的镜像。migrate -path migrations -database "$DSN" down 1;不能反向时按发布说明写的那一步停下,不要现场决定,更不要手工删列。curl -fsS http://localhost:${WEB_PORT:-8080}/healthz,确认返回 200,且错误条数回到基线。docs/decisions/ 记录(不改代码也要留)。main 上用一次 fix 提交修问题,重发下一个修订号。不靠长期停在旧版本运行。# 发布 v1.5.0 - 时间:2026-11-12 - 本次发布:v1.5.0 - 回滚到:v1.4.2 - 数据库:迁移 0009 可反向,回滚时执行 down 1 (若不可反向,写清停在哪一步,例如「停在 0009,不执行 down」) - 发布前基线:近 10 分钟 ERROR 条数 0 ## 发布后核对 - GET /healthz 200 通过 - GET /notes 200 通过 - 跨所属范围 GET /notes 404 通过 - cross-scope access denied 记录 未查
多条迁移怎么退:down 1 只回滚最后一条迁移。这次发布带了三条迁移时,要按 0009 → 0008 → 0007 的顺序逐条反向,顺序写反会撞上外键。迁移命令的参数以官方文档为准。
版本号打 v1.5.0 还是 1.5.0,这类评审不用讨论。要看的是下面六类——它们都会在真出事的那一天变成代价。
| 现象 | 说明什么 | 处理 |
|---|---|---|
| 发布说明里没有「回滚到」这一行 | 出事时无路可退 | 挡回,要求补上这一行再发 |
| 迁移不能反向,但发布说明没写停在哪一步 | 回滚时只能现场决定改数据库 | 要求补上停在哪个编号,见数据 |
版本号跳号,例如 v1.2.0 直接到 v1.4.0 | 中间版本没发布却占用了号,或有人在拍脑袋 | 按提交类型重新推导;确实要跳就写明原因 |
| 标签打在功能分支的提交上 | 回滚时找不到对应 main 快照 | 在 main 上重打;旧标签未推送时删掉,已推送则另发一个修订号 |
同一标签被 git tag -f 移动过 | 已发布的产物与代码不再对应 | 不移动已推送的标签,改发下一个修订号 |
| 核对清单里写「观察一段时间」「体验正常」 | 没有阈值,观察不出结论,也无法写成「未查」 | 改成数值与时间窗,用本页的三个阈值 |
标签能不能重打:第四条里标签还没推送时可以删掉重打,这是常见且安全的操作;一旦推送过,其他人可能已经拉到,此时按第五条处理。判断有没有推送,用 git ls-remote --tags origin 确认。
## v0.3.0 - 更新了若干功能 - 修复了一些问题 - 优化性能
认它的办法:现在立刻回滚,版本号、镜像、数据库停在哪一步,这三样是不是都写得出来。上面三条一条都对不上:没有版本号对应的镜像、没有迁移跑到了第几步、也没有回滚点。这类说明多半是把提交信息拼起来的——提交信息是「update」时,它自然就成了「更新了若干功能」。
git ls-remote --tags origin 里同一版本号只出现一次。违反后果:两次不同的代码对同一个版本号,回滚时不知道该回到哪一份。不用回补历史版本号:第一条里的「逐个 +1」在仓库还没建立标签习惯时,可以先从当前版本起算,不必回补历史版本号。回补历史版本号本身比不做更容易出错。