Lesson 0024:版本发布——让每一次交付都有名有姓

上一课你学会了用 Issues 和 PR 协作,用 Branch Protection 守门。但 CI 通过、代码合并之后——怎么告诉世界「这个版本可以用了」?

这节课教你 DevOps 工程师的发布基本功:语义化版本(SemVer)、Git Tag、GitHub Releases、Changelog、以及自动化发布的 Conventional Commits 体系。

1. Semantic Versioning(语义化版本)——为什么版本号不能随便写

SemVer 2.0.0semver.org)定义了一套版本号的精确规则:

MAJOR.MINOR.PATCH
  │     │     └─ 修 bug,行为不变(Bug fix)
  │     └─────── 加功能,向后兼容(Feature)
  └───────────── 不兼容的 API 变更(Breaking change)

v2.4.1  →  Major=2, Minor=4, Patch=1

什么时候升哪个号?

变更类型版本变化例子
修了一个 bug,API 没变1.3.0 → 1.3.1(PATCH)修复日志格式错误
加了一个新功能,老 API 还能用1.3.1 → 1.4.0(MINOR)新增 /api/v1/export
改了接口,老调用方会挂1.4.0 → 2.0.0(MAJOR)删掉了 /api/v1/old-endpoint

预发布版本

在 PATCH 后面加 - 和标识符:

1.0.0-alpha.1    # 内部测试
1.0.0-beta.2     # 外部早期测试
1.0.0-rc.1       # Release Candidate,不出意外就是最终版了
SemVer 的铁律:一旦发布了 v1.0.0,你不能修改它指向的代码。发现 bug?发 v1.0.1。版本号是不可变的标签——这就是为什么 git tag 之后不应该 force push。

2. Git Tag —— 给 commit 起名

Tag 是一个指向特定 commit 的不可变引用(不像 branch 会移动)。两种 tag:

类型命令内容用哪个?
Lightweightgit tag v1.0.0只有一个 ref 指针❌ 临时用
Annotatedgit tag -a v1.0.0 -m "..."含 tagger、日期、message、GPG 签名永远用这个
# 创建一个 annotated tag
git tag -a v1.0.0 -m "Release v1.0.0: initial stable release"

# 查看 tag 详情(annotated 才有这些信息)
git show v1.0.0
# Tagger: bram35mm <...>
# Date:   Sat Jul 19 2026
#
# Release v1.0.0: initial stable release

# 推送 tag 到 GitHub
git push origin v1.0.0        # 推送单个 tag
git push origin --tags         # 推送所有本地 tag(小心别把旧 tag 也推上去了)

# 删除远程 tag(如果搞错了)
git push origin --delete v1.0.0
git tag -d v1.0.0              # 同时删除本地的
DevOps 场景:CI/CD 流水线用 tag push 作为部署触发器。在 GitHub Actions 里:on: push: tags: ['v*']——推一个 v1.0.0 tag,自动触发构建+测试+发布+部署。

3. GitHub Releases —— Tag 的可分享版本

GitHub Release 是基于 tag 的发布单元。它 = tag + release notes + 可下载的二进制/assets + 讨论区。

在 GitHub Web UI 上创建

  1. 右侧 Releases → Create a new release
  2. 选一个已有的 tag(或现场创建)
  3. 写 Release title(如 v1.0.0 — Initial Stable Release
  4. 写 Release notes。点「Generate release notes」让 GitHub 自动生成(基于合并的 PR)
  5. 如果是测试版,勾选「Set as a pre-release」
  6. 可以上传编译好的二进制、tar.gz、deb 包等作为 assets

用 GitHub Actions 自动创建 Release

# .github/workflows/release.yml
name: Release

on:
  push:
    tags: ['v*']

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Create Release
        uses: softprops/action-gh-release@v2
        with:
          generate_release_notes: true    # 自动生成 release notes
          prerelease: ${{ contains(github.ref, 'rc') || contains(github.ref, 'beta') }}
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
semantic-release 是这个模式的工业级实现:你只需用 Conventional Commits 格式写 commit message,push 到 main 后它自动:分析 commit → 决定版本号 → 生成 changelog → 创建 git tag → 创建 GitHub Release → 发布到 npm/Docker Hub/任何地方。Lesson 0025 的练习会用到。

4. CHANGELOG.md —— 给人看的版本日志

SemVer 的数字告诉用户「改了多少」,Changelog 告诉用户「改了什么」

keepachangelog.com 定义了标准格式:

# Changelog

All notable changes to this project will be documented in this file.

## [1.1.0] - 2026-07-15

### Added
- `POST /api/v1/export` endpoint for data export
- Docker healthcheck in Dockerfile

### Changed
- Upgraded Node.js base image from 22.1 to 22.7

### Deprecated
- `GET /api/v1/legacy` — use `GET /api/v2/data` instead

### Fixed
- Race condition in WebSocket reconnect logic (#142)

### Security
- Patched ReDoS vulnerability in path-to-regexp (#156)

## [1.0.0] - 2026-06-01

### Added
- Initial release with core API

7 个标准分类(只用你需要的):

Added新功能
Changed已有功能的变更
Deprecated即将移除的功能
Removed已移除的功能
FixedBug 修复
Security安全漏洞修复
Docs仅文档变更(非标准,但很多项目用)

5. Conventional Commits —— 让机器读懂你的 commit

Conventional Commits 是标准化的 commit message 格式。它 = SemVer + 自动化发布之间的桥梁。

<type>[optional scope]: <description>

[optional body]

[optional footer(s)]
feat: add user login system               # → MINOR bump
fix: resolve navbar bug on mobile          # → PATCH bump
feat!: redesign API structure              # → MAJOR bump(注意 ! 号)

# 或者用 footer 声明
feat: redesign API structure

BREAKING CHANGE: removed /api/v1/old-endpoint, use /api/v2 instead
type含义版本影响
feat:新功能MINOR
fix:修 bugPATCH
feat! / 含 BREAKING CHANGE:破坏性变更MAJOR
docs:仅文档不触发版本
chore:构建/工具变更不触发版本
ci:CI/CD 配置不触发版本
refactor:重构不触发版本
test:测试不触发版本

为什么 DevOps 要关心这个?因为当你的团队用 Conventional Commits + semantic-release,每次 push 到 main:

git push → CI 跑 → semantic-release 分析 commit → 
自动决定版本号 → 生成 CHANGELOG → git tag → GitHub Release → 部署

零手动操作。这就是 「push on main, ship it」。

动手练习

目标:在一个测试仓库里走完完整的发布流程。

# 1. 准备一个测试仓库
cd /tmp
mkdir release-demo && cd release-demo && git init
echo "# Release Demo" > README.md
git add . && git commit -m "docs: initial commit"

# 2. 模拟几个 feature 和 fix
echo "export PORT=3000" > .env.example
git add . && git commit -m "feat: add port configuration"

echo 'console.log("server starting")' > index.js
git add . && git commit -m "feat: add entry point"

# 修复一个 bug
echo 'console.log("server starting on port", process.env.PORT)' > index.js
git add . && git commit -m "fix: log port number on startup"

# 3. 用 git log 回顾一下
git log --oneline
# a1b2c3d fix: log port number on startup
# e4f5g6h feat: add entry point
# i7j8k9l feat: add port configuration
# m0n1o2p docs: initial commit

# 4. 创建 annotated tag
git tag -a v1.0.0 -m "Release v1.0.0: first stable release"

# 5. 查看 tag
git show v1.0.0

# 6. 写 CHANGELOG(手动写一次,体会格式)
cat > CHANGELOG.md << 'EOF'
# Changelog

## [1.0.0] - 2026-07-19

### Added
- Port configuration via `.env.example`
- Application entry point with port logging

EOF

git add CHANGELOG.md && git commit -m "docs: add CHANGELOG for v1.0.0"

# 7. 如果这是真实项目,接下来:
# git push origin main
# git push origin v1.0.0
# → GitHub 上会自动触发 Release workflow(如果你配了上面的 Actions)
# → 去 Releases 页面手动创建或等 CI 自动创建

小测验

Q1:v1.2.3 → 加了一个向后兼容的新 API endpoint,版本号应变成?
Q2:为什么要用 annotated tag 而不是 lightweight tag?
Q3:commit feat!: remove old auth system 应该触发哪个版本号变化?
Q4:在 keepachangelog 格式中,新增功能放在哪个分类下?

推荐资源

下一步

版本发布体系建好了。但一个开源项目不止有代码——最后一课 开源项目维护(0025),你将学习社区治理、许可证选择、GitHub Pages 文档站,以及用 .github 仓库统一管理组织级配置。

如果你在自己的项目里试了 tag → push → 没触发 CI?检查 Actions workflow 的 trigger:on: push: tags: ['v*'] 还是 on: [push]。只有前者在 tag push 时触发。
← Lesson 0023 · 术语表 · Mission · Lesson 0025 →