一. 前言:部署成功,不等於版本已經說清楚 #
功能合進 main、CI 全綠、服務也上線了。
過兩週有人問:「Production 現在到底是哪一版?」
如果答案是「大概是上上週五那顆 commit」,發版流程就還少了一塊。
Git tag 的工作,是替某個 Git 物件留下穩定名稱;Release 則把這個版本整理成使用者看得懂、拿得到、能追蹤的交付物。
今天拍拍君會把流程串起來:
- 分清楚 lightweight 與 annotated tag
- 用 SemVer 決定下一個版本號
- 在正確 commit 建立、檢查並推送 tag
- 視需求替 tag 加上密碼學簽章
- 產生 release notes,交接到 GitHub Release
- 處理打錯 tag,而不是偷偷覆寫歷史
這篇不討論怎麼設計 branch;如果你還在整理
main、feature 與 hotfix,可以先看 Git Branch 策略。
二. Tag 不是 Branch:先建立正確心智模型 #
Branch 與 tag 都是 Git reference,但用途不同。
- branch 會隨新 commit 前進
- tag 通常固定指向某個已確認版本
- commit ID 能精確定位,但不適合人類記憶
- Release 以 tag 為基礎,再加上說明與附件 可以把它想成:branch 是持續移動的書籤,tag 是出版後印在版權頁上的版次。 先看看目前有哪些 tag:
git tag --list
依版本語意排序,不要只做字串排序:
git tag --list 'v*' --sort=-version:refname
如果想看 tag 指向哪顆 commit:
git show-ref --tags
只看簡潔版本:
git log --oneline --decorate --tags --simplify-by-decoration
注意:一般 git push 不會自動把所有本機 tag 都送出去。
這個分離其實是保護機制,避免你把實驗用標記一起公開。
三. Lightweight 與 Annotated Tag 怎麼選? #
最短的建立方式是:
git tag v1.4.0
這是 lightweight tag,本質上只是 refs/tags/v1.4.0 指向某個物件。
它適合:
- 本機暫時標記
- 短期測試點
- 不需要作者、時間與說明的私人 reference 正式版本更適合 annotated tag:
git tag -a v1.4.0 -m 'Release 1.4.0'
Annotated tag 會建立獨立的 tag object,保存 tagger、時間、訊息,以及可選的簽章。 檢查差異:
git cat-file -t v1.4.0
git cat-file -p v1.4.0
Annotated tag 的第一個指令會輸出 tag;lightweight tag 通常會直接解析成 commit。
正式 release 的實用預設很簡單:
對外版本用 annotated tag;一次性私人標記才用 lightweight tag。 Git 官方文件也把 annotated tag 定位為 release 用途,理由不是儀式感,而是它攜帶可稽核的版本資訊。
四. SemVer:版本號是在描述相容性 #
Semantic Versioning 的核心格式是:
MAJOR.MINOR.PATCH
判斷方式:
| 變更 | 升哪一段 | 範例 |
|---|---|---|
| 相容的 bug fix | PATCH | 1.4.2 → 1.4.3 |
| 相容的新功能 | MINOR | 1.4.3 → 1.5.0 |
| 不相容的公開 API 變更 | MAJOR | 1.5.0 → 2.0.0 |
| 重點是「公開 API」。 | ||
| 它可能是 library 的函式介面、CLI 參數、HTTP schema、設定檔格式,甚至使用者依賴的輸出行為。 | ||
| 只看 commit 數量決定升版,通常會得到很有氣勢但沒有資訊量的版本號。 |
4.1 v 是 tag 命名慣例,不是 SemVer 本體
#
1.4.0 是 SemVer;v1.4.0 是常見的 Git tag 名稱。
團隊可以選擇加不加 v,但必須固定:
推薦:v1.4.0, v1.4.1, v1.5.0
避免:v1.4.0, 1.4.1, release-1.5
一致命名會讓 CI pattern、排序與 changelog 工具簡單很多。
4.2 Pre-release 與 Build Metadata #
測試版本可以寫成:
1.5.0-alpha.1
1.5.0-beta.2
1.5.0-rc.1
它們的優先序都低於正式的 1.5.0。
Build metadata 用 +:
1.5.0+build.20260912
它不參與版本優先序比較。 還有一條很重要:版本一旦發布,內容就不該被修改;修正要用新版本交付。 這條規則跟「不要移動已公開 tag」剛好互相呼應。
五. 發 Tag 前:先證明 HEAD 值得被命名 #
不要把 tag 當成「觸發 CI 的按鈕」。 它首先是一份承諾:這顆 commit 就是指定版本。 先同步遠端狀態:
git fetch origin --prune --tags
git status --short --branch
確認目前 commit:
git log -1 --show-signature
git rev-parse HEAD
git rev-parse origin/main
如果 release 必須來自 main,兩個 rev-parse 結果就應符合團隊政策。
接著跑專案檢查。以下只是示意,請換成自己的命令:
make lint
make test
make build
再確認版本號尚未存在於本機與遠端:
git tag --list 'v1.4.0'
git ls-remote --exit-code --tags origin 'refs/tags/v1.4.0'
第二個指令找不到時會回傳非零狀態,寫 release script 時要明確處理,不要把「不存在」誤判成網路故障。 最後列出上一版到現在的範圍:
git log --oneline v1.3.2..HEAD
git diff --stat v1.3.2..HEAD
這一步很常抓到漏掉的文件、migration 或不該進 release 的 debug commit。
六. 建立、檢查與推送 Annotated Tag #
確認無誤後建立版本:
git tag -a v1.4.0 -m 'Release 1.4.0'
預設會 tag 當前 HEAD。
如果要明確指定 commit,請把 commit ID 寫出來:
git tag -a v1.4.0 4f21c7a -m 'Release 1.4.0'
建立後不要急著 push,先檢查:
git show --stat v1.4.0
git rev-list -n 1 v1.4.0
git merge-base --is-ancestor v1.4.0 origin/main
最後一行可確認 tag 指向的 commit 是否已在遠端 main 歷史中。
只推這一個 tag:
git push origin tag v1.4.0
或使用等價的完整 ref:
git push origin refs/tags/v1.4.0
拍拍君偏好逐一推送 release tag,因為意圖最清楚。
git push --tags 會推所有 tag;repo 裡若混有本機實驗標記,範圍可能太大。
git push --follow-tags 則只會附帶可從被推分支到達、且遠端缺少的 annotated tags,比 --tags 收斂,但自動化前仍要了解範圍。
七. Signed Tag:驗證「誰替這版背書」 #
Annotated tag 能保存作者資訊,但名字與 email 本身不是密碼學證明。 需要供應鏈驗證時,可以簽署 tag:
git tag -s v1.4.0 -m 'Release 1.4.0'
驗證簽章:
git tag --verify v1.4.0
Git 支援的 signing backend 由 gpg.format 控制,常見選擇包含 OpenPGP 與 SSH。
以 SSH signing 為例,基本設定可能像這樣:
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
若團隊要求所有 tag 都簽署:
git config --global tag.gpgSign true
但先別在 CI 裡盲目打開。 自動簽署牽涉 key custody、agent、權限與 rotation;保護私鑰比多一個綠色標記重要。 另外,signed tag 與 signed push 不同:
- signed tag 驗證 tag object
- signed push 驗證一次 ref 更新請求 大多數 release 流程首先需要的是 signed tag。
八. Release Notes:替使用者整理差異 #
Commit log 是開發紀錄,不是 release notes。 一份實用說明至少包含:
## Highlights
- 新增批次匯入模式
## Fixes
- 修正逾時後重試可能重複送出的問題
## Breaking changes
- `--config` 改為讀取 TOML,舊 JSON 格式不再支援
## Upgrade notes
- 升級前先執行 `pypy-tool migrate --check`
## Full changelog
- v1.3.2...v1.4.0
產生原始素材:
git shortlog --summary --numbered v1.3.2..v1.4.0
git log --first-parent --oneline v1.3.2..v1.4.0
--first-parent 對以 merge commit 合併 PR 的 repo 很實用,可以減少分支內部 commit 的噪音。
Release notes 要回答的是:
- 使用者得到什麼?
- 有沒有破壞性變更?
- 要不要遷移資料或設定?
- 哪些問題已知但尚未修好?
- 如何從上一版升級或回復? 如果只是把 87 行 commit subject 原封不動貼上去,那叫 log dump,不叫交接。
九. 從 Git Tag 交接到 GitHub Release #
Git tag 已經能標記版本;GitHub Release 再補上可閱讀頁面、自動產生的 source archive、release notes 與可下載附件。 先用 draft 準備內容:
gh release create v1.4.0 \
--draft \
--title 'v1.4.0' \
--notes-file RELEASE_NOTES.md \
--verify-tag
檢查頁面、附件與 notes 後再發布:
gh release edit v1.4.0 --draft=false
若要附上 build artifact:
gh release upload v1.4.0 dist/*
附件要先由 CI 建置、驗證 checksum,最好也保留 provenance;不要在不同電腦手工編出幾份名字相同、內容不同的檔案。 若 repo 啟用 immutable releases,先建 draft、備齊附件再發布尤其重要,因為發布後對 tag 與 assets 的修改會受限制。 GitHub Release 不是 Git tag 的替代品;它是建立在 tag 上的發佈介面。
十. 打錯 Tag 怎麼辦?先看有沒有公開 #
10.1 尚未 Push:本機修正 #
如果 tag 還沒離開你的電腦,處理很單純:
git tag -d v1.4.0
git tag -a v1.4.0 <correct-commit> -m 'Release 1.4.0'
再重新檢查內容。
10.2 已 Push、尚未被使用:協調後刪除 #
先確認團隊與自動化沒有消費它,再刪遠端:
git push origin --delete v1.4.0
git tag -d v1.4.0
建立正確 tag 後重新推送,並在團隊頻道清楚說明舊 tag 曾被替換。
10.3 已經發布:不要移動,發新版本 #
如果 package、binary、container 或使用者已經拿到這一版,最安全的方式通常是:
v1.4.0 有問題 → 修正 commit → 發 v1.4.1
不要讓同一個版本名稱今天指向 A、明天指向 B。 遠端 tag 更新預設會被拒絕;雖然可以 force,但「做得到」不代表「應該做」。 已發布版本的可重現性,比把版號修得看起來漂亮更重要。
十一. 一份可執行的 Release Checklist #
把流程寫進 repository,不要只放在某位同事腦中。
[ ] main 已同步,工作目錄乾淨
[ ] lint、test、build 全部通過
[ ] 版本號依公開 API 變更決定
[ ] changelog、文件、migration 已更新
[ ] 新 tag 在本機與遠端都不存在
[ ] 候選 commit 已在受保護分支
[ ] 建立 annotated 或 signed tag
[ ] 本機驗證 tag 內容與簽章
[ ] 明確推送單一 tag
[ ] CI 從該 tag 建置一次
[ ] Release notes 與附件已檢查
[ ] 發布後做乾淨環境 smoke test
[ ] 保留 rollback 與事故處理說明
如果 tag push 會直接觸發 production,還可以加入 environment approval,讓「版本已命名」與「允許部署」成為兩個可稽核步驟。
十二. 常見錯誤 #
12.1 在錯的 Branch 打 Tag #
Tag 最終指向的是 commit,不是 branch 名稱。
發版前用 git rev-parse、git show 與 merge-base --is-ancestor 檢查,不要只看終端機 prompt。
12.2 正式版只用 Lightweight Tag #
它不是壞掉,但少了 tagger、時間、說明與簽章空間。 對外 release 用 annotated tag 會留下更完整的稽核資訊。
12.3 每次都 git push --tags
#
這可能把所有本機 tag 一起送出。
正式流程優先推明確名稱;要自動跟隨 annotated tags 時,再有意識地使用 --follow-tags。
12.4 把 Tag 當備份 #
Tag 是 reference,不是異地備份。 Repository、CI artifact、registry 與簽章金鑰仍然需要各自的保存策略。
12.5 先發布,再想 Release Notes #
等使用者已經升級才補 breaking changes,驚喜就會變成事故。 先建立 draft release,完成交接資訊,再按下公開。
十三. 最小安全流程 #
如果團隊今天只想先做一個能落地的版本,可以從這組開始:
git fetch origin --prune --tags
git status --short --branch
make test
git log --oneline v1.3.2..HEAD
git tag -a v1.4.0 -m 'Release 1.4.0'
git show --stat v1.4.0
git push origin tag v1.4.0
gh release create v1.4.0 \
--title 'v1.4.0' \
--notes-file RELEASE_NOTES.md \
--verify-tag
等這條路徑穩定後,再逐步加入 signed tag、artifact checksum、provenance、approval 與 immutable release。 流程越成熟,發版應該越無聊。 無聊代表每一步都可預期,而不是半夜收到「這個 v1.4.0 到底是哪一顆?」
結語 #
Git tag 不是替 commit 換一個好看的名字,而是 release lifecycle 的固定錨點。 拍拍君建議記住四件事:
- 正式版本用 annotated tag,需要驗證來源時再簽署
- SemVer 要描述公開 API 的相容性,不是描述今天心情
- 發 tag 前檢查 commit,推送時明確指定單一 tag
- 已發布版本不要移動;有錯就修正並發新版本 當 tag、CI artifact、release notes 與部署紀錄都指向同一顆 commit,回溯問題與交接版本就不再靠猜。