蓋上戳記!所有程式都必須回報自己的版本
原文由 Michael Stapelberg 于 發布,訂閱此部落格
前陣子在處理一次線上事故時,我在不到一小時內就正確猜中了故障的根本原因(不錯吧!),也送出了一個修正來驗證這個假設,結果卻因為看不到版號和部署狀況,在黑暗中摸索了好幾個小時…… 😞
這次經驗讓我再次思考軟體版本管理的問題,更精確地說,是關於建置資訊(build versioning、version stamping,怎麼稱呼都可以)和版本回報。我才意識到,早在十多年前,我在 i3 視窗管理器中就已經把這個問題解決得很好了,所以在工作中發現這個問題竟然完全沒被解決,真的讓我很意外。
在這篇文章中,我會說明只要做到 3 個簡單的步驟(蓋上去!串接起來!回報出來!),就能在事故處理時幫你省下數小時的延誤與壓力。
為什麼我們的版本管理標準這麼低?!
每一台家電都有極其詳細的版本標示!看看這台洗碗機:

(感謝 Feuermurmel 提供了這個很棒的範例!)
我看過幾次家電維修,給我的感覺是,如果維修人員無法辨識這台機器,他們很可能連碰都不會碰。
相較之下,為什麼我們在電腦領域的標準就這麼低呢?當然,消費性產品通常還是會用某種方式標上版本,而且通常也夠用了(除了像 USB 3.2 Gen 1×2 這種例子之外!)。但最近,我卻遇到太多沒有做好版本標示的開發版建置!
軟體版本管理
不像家電那樣有一塊刻好資訊的金屬銘牌,軟體會不斷更新,而且運行在我們常常根本看不見的地方與架構之中。
讓我們來深入看看,要提升版本管理的水準,我們需要什麼!
通常,軟體會有一個名稱和一個精細程度不一的版號:
- Chrome
- Chrome 146
- Chrome 146.0.7680.80
- Chrome f08938029c887ea624da7a1717059788ed95034d-refs/branch-heads/7680_65@{#34}
以上這些都能指稱我電腦上的 Chrome 瀏覽器,只是精細程度不同。
這些說法都正確,也都有用,端看使用情境。以下是各自的範例:
- 「在我這邊用 Chrome 是正常的,你有用 Firefox 測過嗎?」
- 「Chrome 146 有個壞掉的中鍵貼上並導覽功能」
- 「我用的是 Chrome 146.0.7680.80,無法重現你的問題」
- 「在 Chrome f08938029c887ea624da7a1717059788ed95034d-refs/branch-heads/7680_65@{#34} 這個版本上套用這個 patch,然後照這些步驟重現:[…]」
在打造了i3 視窗管理器之後,我很快就學到,對使用者支援來說,程式能清楚表明自己的身分是非常有價值的。讓我用下面的案例來說明。
案例研究:i3 的 --version 與 --moreversion
執行 i3 --version 時,你會看到類似這樣的輸出:
% i3 --version
i3 version 4.24 (2024-11-06) © 2009 Michael Stapelberg and contributors
每一個字都是經過仔細斟酌才放上去的。讓我來拆解一下:
i3 version 4.24:我本來可以簡寫成i3 4.24或i3 v4.24,但我覺得明確一點比較好,因為i3這個名字實在太短了。使用者可能會喃喃自語「i-3-4-2-4 是什麼?」,但加上「version」這個字,就暗示了 i3 是某種電腦相關的東西(→ 一個電腦程式),而它的版本是 4.24。(2024-11-06)是發行日期,讓你能馬上判斷「4.24」是不是夠新。© 2009 Michael Stapelberg標示了專案的起始時間以及背後的主要人物。and contributors則是向眾多協助過的人致謝。i3 從來就不是一個人的專案,它一直是團隊合作的成果。
在做使用者支援時,有幾個問題在概念上很容易向遇到問題的使用者提問,卻能為開發者帶來非常有價值的答案:
- 問題:「你用的是哪個版本的 i3?」
- 由於 i3 不是那種在視窗中執行的一般程式(而是視窗管理器/桌面環境),所以沒有「說明 → 關於」這種選單選項。
- 因此,我們改為詢問:
i3 --version的輸出是什麼?
- 問題:「你回報的是新問題還是原本就存在的問題?為了確認,你能試著回到之前使用的 i3 版本嗎?」。「回到舊版」的專業術語是 downgrade、rollback 或 revert。
- 取決於你用的 Linux 發行版,這件事可能非常簡單,也可能是場惡夢。
- 用 NixOS 的話就很簡單:只要在開機選單中選擇舊的系統「generation」開機就行了。或者,如果你的設定檔有用 git 做版本控管,直接 revert 回去也可以。
- 對於像 Debian Linux 或 Arch Linux 這種指令式(imperative)的發行版,如果你沒有做檔案系統層級的快照,升級後就沒有簡單又可靠的方法可以回到舊版。運氣好的話,你可以直接用
apt install安裝舊版的 i3。但你可能會遇到相依性衝突(「版本地獄」)。 - 我知道用snapshot.debian.org 是可以跑舊版 Debian 的,但至少就我上次嘗試的經驗來說,並不是很實用。
- 你能幫忙確認一下這個問題在最新的 i3 開發版中是否依然存在嗎?
- 當然,我也可以自己試著用最新的發行版重現使用者回報的問題,然後再多一次用最新的開發版重現。
- 但把驗證的步驟交給回報問題的使用者來做會更好,因為這樣可以篩選出動機強烈的回報者(這種回報更有機會真的促成修正!),而且會讓使用者把 bug 重現兩次,藉此判斷這是不是偶發性問題、是否難以重現、重現步驟是否正確等等。
- 接下來很自然的追問就是:「這個程式碼修改有讓問題消失嗎?」對於現在已經有開發環境的當事使用者來說,這很容易測試。
根據我多次提出這些問題的經驗,我注意到這些除錯過程中有一些固定的模式。因此,我在 i3 v4.3(於 2012 年 9 月發布)中為 i3 新增了另一種回報版本的方式:--moreversion 參數!現在我可以問使用者第一個問題的稍微變化版:i3 --moreversion 的輸出是什麼?注意,這個問法連用口語傳達也很順暢,例如在電腦聚會上:
Michael: 你用的是哪個版本?
User: 要怎麼看?
Michael: 執行這個指令:
i3 --versionUser: 它顯示 4.24。
Michael: 很好,這個版本夠新,已經包含那個修正了。現在,我們需要更詳細的版本資訊!麻煩執行
i3 --moreversion,告訴我你看到了什麼。
當你執行 i3 --moreversion 時,它不只會回報你所呼叫的那個 i3 程式的版本,還會透過IPC(行程間通訊)介面連線到你 X11 工作階段中正在執行的 i3 視窗管理器處理程序,並回報正在執行中的 i3 處理程序的版本,同時顯示其他對使用者有幫助的關鍵資訊,例如載入了哪個設定檔、以及它最後是什麼時候被修改的:
% i3 --moreversion
Binary i3 version: 4.24 (2024-11-06) © 2009 Michael Stapelberg and…
Running i3 version: 4.24 (2024-11-06) (pid 2521)
Loaded i3 config:
/home/michael/.config/i3/config (main)
(last modified: 2026-03-15T23:09:27 CET, 1101585 seconds ago)
The i3 binary you just called:
/nix/store/0zn9r4263fjpqah6vdzlalfn0ahp8xc2-i3-4.24/bin/i3
The i3 binary you are running: i3
乍看之下這份輸出似乎資訊量很大,但讓我來說明為什麼它是如此有價值的除錯工具:
透過 IPC 介面連線到 i3 本身就是一項很有意義的測試。如果使用者能看到
i3 --moreversion的輸出,就代表他們應該也能執行像是i3-msg -t get_tree > /tmp/tree.json這類除錯指令來擷取完整的版面配置狀態。在除錯過程中,執行
i3 --moreversion可以輕鬆確認你剛剛建置的版本是否真的生效了(請看Running i3 version那一行)。- 請注意,這正是線上事故處理時也很關鍵的檢查:驗證實際正在執行的版本是否與預期應該執行的版本一致。
顯示所載入設定檔的完整路徑,能讓人一眼就看出使用者是否改錯了檔案。如果光看路徑還不夠,修改時間(同時以絕對時間與相對時間顯示)也能幫忙揪出改錯檔的情況。
順帶一提,我用的是 NixOS,所以會自動為這一次特定的 i3 建置取得一個穩定的識別碼(0zn9r4263fjpqah6vdzlalfn0ahp8xc2-i3-4.24)。
% ls -l $(which i3)
lrwxrwxrwx 1 root root 58 1970-01-01 01:00 /run/current-system/sw/bin/i3
-> /nix/store/0zn9r4263fjpqah6vdzlalfn0ahp8xc2-i3-4.24/bin/i3
要查看產生這個 Nix store 輸出(0zn9r4263…-i3-4.24)的建置配方(在 Nix 術語中稱為「derivation」),我可以執行 nix derivation show:
% nix derivation show /nix/store/0zn9r4263fjpqah6vdzlalfn0ahp8xc2-i3-4.24
{
"/nix/store/z7ly4kvgixf29rlz01ji4nywbajfifk4-i3-4.24.drv": {
[…]
點此展開完整的 nix derivation show 輸出(如果你好奇的話)
% nix derivation show /nix/store/0zn9r4263fjpqah6vdzlalfn0ahp8xc2-i3-4.24
{
"/nix/store/z7ly4kvgixf29rlz01ji4nywbajfifk4-i3-4.24.drv": {
"args": [
"-e",
"/nix/store/l622p70vy8k5sh7y5wizi5f2mic6ynpg-source-stdenv.sh",
"/nix/store/shkw4qm9qcw5sc5n1k5jznc83ny02r39-default-builder.sh"
],
"builder": "/nix/store/6ph0zypyfc09fw6hlc1ygjvk2hv4j9vd-bash-5.3p3/bin/bash",
"env": {
"NIX_MAIN_PROGRAM": "i3",
"name": "i3-4.24",
"out": "/nix/store/0zn9r4263fjpqah6vdzlalfn0ahp8xc2-i3-4.24",
"version": "4.24"
},
[… jakiego}
}
可惜的是,據我所知目前沒有辦法從 derivation 反推回 .nix 原始碼,但至少可以檢查某個原始碼是否會產生完全相同的 derivation。
開發版建置
到目前為止我所描述的版本管理方式,對大多數只關心正式發行版、而不會去追蹤軟體中間版本的使用者來說已經足夠了。
但對於開發者,或是任何需要更高精確度的使用者來說呢?
當從 git 建置 i3 時,它會使用git-describe(1) 來回報它是從哪個 git revision 建置而來的:
~/i3/build % git describe
4.25-23-g98f23f54
~/i3/build % ninja
[110/110] Linking target i3
~/i3/build % ./i3 --version
i3 version 4.25-23-g98f23f54 © 2009 Michael Stapelberg and contributors
如果工作目錄有被修改過,會在 revision 後面加上一個 + 來表示:
~/i3/build % echo '// dirty working copy' >> ../src/main.c && ninja
[104/104] Linking target i3bar
~/i3/build % ./i3 --version
i3 version 4.25-23-g98f23f54+ © 2009 Michael Stapelberg and contributors
回報 git revision(或更廣義來說,VCS revision)是最有用的選擇。
這樣一來,我們就能抓到以下常見的錯誤:
- 大家從錯誤的 revision 建置。
- 建置了,卻忘了安裝。
- 安裝了,但目前的工作階段沒有載入到(裝錯位置?)。
最有用的做法:蓋上 VCS Revision 的戳記
如同我們在上面看到的,單一最有用的版本資訊就是 VCS revision。我們可以從 VCS 儲存庫中取得所有其他細節(版號、日期、作者……)。
現在,讓我們透過 Go 的做法來展示最理想的情況!
Go 預設就會蓋戳記! 🥳
Go 這些年來已經成為我最喜愛的程式語言,很大一部分是因為 Go 開發團隊的好品味與風格,當然還有高品質的工具:
因此,我很高興可以說,Go 在軟體版本管理方面做到了黃金標準:它預設就會嵌入 VCS 建置資訊! 🥳 這是在Go 1.18(2022 年 3 月)中引入的:
此外,go 指令會嵌入關於建置的資訊,包含建置與工具標籤(透過 -tags 設定)、編譯器、組譯器與連結器的旗標(例如 -gcflags)、是否啟用了 cgo,以及如果有啟用,cgo 環境變數(如 CGO_CFLAGS)的值。
VCS 與建置資訊都可以透過
go version -m file或runtime/debug.ReadBuildInfo(針對目前正在執行的二進位檔)或新的debug/buildinfo 套件一起讀取。
註:在 Go 1.18 之前,標準做法是使用 -ldflags -X main.version=$(git describe) 或類似的方式明確注入。這種設定是可行的(而且在許多地方仍可看到),但需要修改應用程式的程式碼,而 Go 1.18 以上的自動蓋戳記則不需要任何額外步驟。
在實務上這代表什麼呢?以下是常見情況的示意圖:從 git 建置:
這涵蓋了我大部分的個人興趣專案!
很多工具我都是直接用 go install 安裝,或者用 CGO_ENABLED=0 go install,這樣就能輕鬆複製到其他電腦上。不過,我現在有越來越多軟體是透過 NixOS 來管理的。
當我發現有程式還沒被完整納入管理時,我可以用 gops 和 go 工具來辨識它:
root@ax52 ~ % nix run nixpkgs#gops
2573594 1 dcs-package-importer go1.26.1 /nix/store/clby54zb003ibai8j70pwad629lhqfly-dcs-unstable/bin/dcs-package-importer
2573576 1 dcs-source-backend go1.26.1 /nix/store/clby54zb003ibai8j70pwad629lhqfly-dcs-unstable/bin/dcs-source-backend
2573566 1 debiman go1.25.5 /srv/man/bin/debiman
[…]
root@ax52 ~ % nix run nixpkgs#go -- version -m /srv/man/bin/debiman
/srv/man/bin/debiman: go1.25.5
path github.com/Debian/debiman/cmd/debiman
mod github.com/Debian/debiman v0.0.0-20251230101540-ac8f5391b43b+dirty
build vcs=git
build vcs.revision=ac8f5391b43bc1a9dbdc99f6179e2fb7d7414a04
build vcs.time=2025-12-30T10:15:40Z
build vcs.modified=true
Go 預設就把事情做對,真的很棒!
由 100% Go 軟體組成的系統(例如我的gokrazy Go 設備平台)就完全都有蓋上戳記!舉例來說,gokrazy 的網頁介面會精確顯示我的scan2drive 設備上 gokrazy/rsync 這個建置包含了哪些版本與相依套件。
儘管已經完整蓋上了戳記,請注意 gokrazy 目前只會顯示模組版本,而不會顯示 VCS 建置資訊,因為它目前也遇到了和 Nix 一樣的落差:

Go 的版本回報
對於採用滾動式發行模式(沒有版號)的 gokrazy packer,我最後寫了幾行 Go 程式碼(見下方),無論你是從 Go 模組安裝 packer,還是從 git 工作目錄安裝,都能顯示 git revision。
這段程式碼會顯示 vcs.revision(簡單的情況;從 git 建置),或是從主模組的 Go 模組版本中擷取出 revision(BuildInfo.Main.Version):
還有哪些情況呢?以下範例說明了我通常會遇到的情境:
| 來源(建置來源) | 建置資訊(蓋到程式上的戳記) |
|---|---|
| 目錄(沒有 git) | 模組 (devel) |
| Go 模組 | 模組 v0.3.1-0.20260105212325-5347ac5f5bcb |
| 目錄(git) | 模組 v0.0.0-20260131174001-ccb1d233f2a4+dirty |
vcs.revision=ccb1d233f2a43e9118b9146b3c9a5ded1efb7551 | |
vcs.time=2026-01-31T17:40:01Z | |
vcs.modified=true |
用 Go 程式碼以程式化方式讀取版本
package version
import (
"runtime/debug"
"strings"
)
func readParts() (revision string, modified, ok bool) {
info, ok := debug.ReadBuildInfo()
if !ok {
return "", false, false
}
settings := make(map[string]string)
for _, s := range info.Settings {
settings[s.Key] = s.Value
}
// When built from a local VCS directory, we can use vcs.revision directly.
if rev, ok := settings["vcs.revision"]; ok {
return rev, settings["vcs.modified"] == "true", true
}
// When built as a Go module (not from a local VCS directory),
// info.Main.Version is something like v0.0.0-20230107144322-7a5757f46310.
v := info.Main.Version // for convenience
if idx := strings.LastIndexByte(v, '-'); idx > -1 {
return v[idx+1:], false, true
}
return "<BUG>", false, false
}
func Read() string {
revision, modified, ok := readParts()
if !ok {
return "<not okay>"
}
modifiedSuffix := ""
if modified {
modifiedSuffix = " (modified)"
}
return "https://github.com/gokrazy/tools/commit/" + revision + modifiedSuffix
}
實際上看起來是這樣:
% go install github.com/gokrazy/tools/cmd/gok@latest
% gok --version
https://github.com/gokrazy/tools/commit/8ed49b4fafc7
但從 git 建置的版本會有完整的 revision 可用(→ 你可以分辨出差異):
% (cd ~gokrazy/../tools && go install ./cmd/...)
% gok --version
https://github.com/gokrazy/tools/commit/ba6a8936f4a88ddcf20a3b8f625e323e65664aa6 (modified)
在 NixOS 上取得 VCS revision
用 Nix 打包 Go 軟體時,很容易就會遺失 Go 的 VCS revision 戳記:
- 像
fetchFromGitHub這類 Nix fetcher 是透過從 GitHub 抓取封存檔(.tar.gz)來實作的——並不會傳輸完整的.git儲存庫,這樣比較有效率。 - 即使有
.git儲存庫存在,Nix 通常也會為了可重現性而刻意將其移除:.git目錄中包含了會在例如執行git gc後改變的 packed objects,這會破壞可重現建置(同樣的原始碼卻有不同的 hash)。
所以這裡根本的張力在於可重現性與 VCS 蓋戳記之間的衝突。
幸好,有一個兩全其美的解法:我建立了stapelberg/nix/go-vcs-stamping Nix overlay 模組,只要匯入它,就能讓你的 buildGoModule Nix 表達式預設就擁有可用的 Go VCS revision 戳記!
Nix Go 建置狀況詳解
提示:如果你不是 Nix 使用者,可以直接跳過這一節。我把這段放在文章中,是為了讓你有一個在最複雜環境中讓 VCS 蓋戳記運作的完整範例。
在 Nix 中打包 Go 軟體其實相當簡單直覺。
舉例來說,Go 的 Protobuf 產生器外掛 protoc-gen-go 在 Nix 中只用了不到 30 行就打包完成:官方 nixpkgs 的 protoc-gen-go package.nix。你只要呼叫buildGoModule,把fetchFromGitHub 的結果當作 src 傳入,再加上幾行 metadata 就好了。
但要讓開發版建置完整蓋上戳記,可就一點也不簡單了!
在打包我自己的軟體時,我想要打包的是個別的 revision(開發版建置),而不只是已發行的版本。我使用同樣的 buildGoModule,如果需要最新版 Go 就用 buildGoLatestModule。我不用 fetchFromGitHub,而是透過 Flakes 來提供原始碼,通常也是來自 GitHub 或其他 Git 儲存庫。例如,我是這樣打包 gokrazy/bull 的:
{
pkgs,
pkgs-unstable,
bullsrc,
...
}:
# Use buildGoLatestModule to build with Go 1.26
# even before NixOS 26.05 Yarara is released
# (NixOS 25.11 contains Go 1.25).
pkgs-unstable.buildGoLatestModule {
pname = "bull";
version = "unstable";
src = bullsrc;
# Needs changing whenever `go mod vendor` changes,
# i.e. whenever go.mod is updated to use different versions.
vendorHash = "sha256-sU5j2dji5bX2rp+qwwSFccXNpK2LCpWJq4Omz/jmaXU=";
}
bullsrc 來自我的 flake.nix:
點此展開完整的 flake.nix
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11";
nixpkgs-unstable.url = "github:nixos/nixpkgs/nixos-unstable";
disko = {
url = "github:nix-community/disko";
inputs.nixpkgs.follows = "nixpkgs";
};
stapelbergnix.url = "github:stapelberg/nix";
zkjnastools.url = "github:stapelberg/zkj-nas-tools";
configfiles = {
url = "github:stapelberg/configfiles";
flake = false;
};
bullsrc = {
url = "github:gokrazy/bull";
flake = false;
};
sops-nix = {
url = "github:Mic92/sops-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs =
{
nixpkgs,
nixpkgs-unstable,
disko,
stapelbergnix,
zkjnastools,
bullsrc,
configfiles,
sops-nix,
...
}:
let
system = "x86_64-linux";
pkgs = import nixpkgs {
inherit system;
config.allowUnfree = false;
};
pkgs-unstable = import nixpkgs-unstable {
inherit system;
config.allowUnfree = false;
};
in
{
nixosConfigurations.keep = nixpkgs.lib.nixosSystem {
inherit system;
inherit pkgs;
specialArgs = { inherit configfiles; };
modules = [
disko.nixosModules.disko
sops-nix.nixosModules.sops
./configuration.nix
{
nixpkgs.overlays = [
(final: prev: {
bull = import ./bull-pkg.nix {
pkgs = final;
pkgs-unstable = pkgs-unstable;
inherit bullsrc;
};
})
];
}
];
};
};
}
Go 會為所有建置蓋戳記,但在這裡它沒什麼東西好蓋:
- 我們是從目錄建置,而不是從 Go 模組,所以模組版本是
(devel)。 - 蓋上去的建置資訊中不包含任何
vcs資訊。
以下是 gokrazy/bull 的完整範例:
% go version -m \
/nix/store/z3y90ck0fp1wwd4scljffhwxcrxjhb9j-bull-unstable/bin/bull
/nix/store/z3y90ck0fp1wwd4scljffhwxcrxjhb9j-bull-unstable/bin/bull: go1.26.1
path github.com/gokrazy/bull/cmd/bull
mod github.com/gokrazy/bull (devel)
dep github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c
build -buildmode=exe
build -compiler=gc
build -trimpath=true
build CGO_ENABLED=0
build GOARCH=amd64
build GOOS=linux
build GOAMD64=v1
要修復 VCS 蓋戳記,請將我的 goVcsStamping overlay 加到你的 nixosSystem.modules 中:
{
nixpkgs.overlays = [
stapelbergnix.overlays.goVcsStamping
];
}
(如果你跟我一樣使用 nixpkgs-unstable,就需要在兩個地方都套用這個 overlay。)
重新建置後,你的 Go 二進位檔應該就會新蓋上 vcs 建置資訊了:
% go version -m /nix/store/z8mgsf10pkc6dgvi8pfnbb7cs23pqfkn-bull-unstable/bin/bull
[…]
build vcs=git
build vcs.revision=c0134ef21d37e4ca8346bdcb7ce492954516aed5
build vcs.time=2026-03-22T08:32:55Z
build vcs.modified=false
太好了!🥳 但……它是怎麼運作的?什麼時候會生效?你要怎麼知道該如何修正你的設定?
我先給你看完整的示意圖,再來解釋該怎麼看:
取決於你在 .nix 檔案中寫了什麼,你會落在 Nix 堆疊中 3 個相關部分之一:
- Fetcher。這是 Flakes 會用到的,但非 Flake 的情境也會用到。
- 固定輸出 derivation(Fixed-output derivation,FOD)。
pkgs.fetchgit就是這樣實作的,但 FOD 固有的 hash 不斷變動(需要一直更新sha256那一行)很惱人。 - Copier。這些只是把檔案複製到 Nix store 中,並沒有 git 感知能力。
為了 VCS revision 蓋戳記的目的,你應該:
- 避免使用 Copier!如果你使用 Flakes:
- ❌ 不要把
url = "/home/michael/dcs"當作 Flake input - ✅ 改用
url = "git+file:///home/michael/dcs"以具備 git 感知能力
- ❌ 不要把
- 我也會避免使用固定輸出 derivation(FOD)。
- 在建置時才抓取 git 儲存庫既慢又沒效率。
- 啟用
leaveDotGit(用這種方式做 VCS revision 蓋戳記時需要)會更沒效率,因為為了保持 FOD 的可重現性,必須以確定性的方式重新建構一個 Git 儲存庫。
因此,我們會堅持使用最左邊那一欄:fetcher。
可惜的是,預設情況下,使用 fetcher 時,VCS revision 資訊雖然儲存在 Nix attrset(在建置過程中的記憶體內)中,卻不會進入 Nix store,因此當 Nix derivation 被求值、Go 編譯原始碼時,Go 看不到任何 VCS revision。
我的stapelberg/nix/go-vcs-stamping Nix overlay 模組修正了這個問題,而啟用這個 overlay 就是讓你走上上圖最左側那條路的方式:那條快樂的路徑,你的 Go 二進位檔現在都有蓋上戳記了!
我的 workaround:Nix git 建置資訊 overlay
go-vcs-stamping overlay 是如何運作的呢?它是作為 Nix 與 Go 之間的轉接器:
- Nix 在
.rev這個記憶體內的 attrset 中追蹤 VCS revision。 - Go 則預期透過存取
.git/HEAD檔案和執行git(1) 指令,在.git儲存庫中找到 VCS revision。
因此,這個 overlay 實作了 3 個步驟來讓 Go 蓋上正確的資訊:
- 合成一個
.git/HEAD檔案,讓 Go 的vcs.FromDir()能偵測到 git 儲存庫。 - 在
PATH中注入一個git指令,它只實作 Go 會用到的兩個指令,其他指令則會大聲地報錯(以防 Go 更新其實作)。 - 在
GOFLAGS環境變數中設定-buildvcs=true。
完整原始碼請見go-vcs-stamping.nix。
乾淨的修正方式
關於以更乾淨的方式修補這個落差,請參見Go issue #77020 與Go issue #64162:讓套件管理器能在呼叫 Go 工具時注入正確的 VCS 資訊。
這樣就能讓 Nix(或 gokrazy)乾淨地傳遞建置資訊,而不需要像我的 go-vcs-stamping 轉接器這種 workaround。
截至撰寫本文時,issue #77020 似乎還沒有太多進展,仍然是 open 狀態。
結論:蓋上去!串接起來!回報出來!
我的論點很簡單:
蓋上 VCS revision 的戳記在概念上很簡單,但非常重要!
舉例來說,如果我提到的那次事故中的線上系統有回報自己的版本,我們就能省下好幾個小時的緩解時間!
可惜的是,許多環境只會標示建置產物(有用,但正交),卻沒有把 VCS revision 串接起來(這可有用多了!),或者至少不是預設就有的。
要修正這個問題,你的行動計畫只需要 3 個簡單步驟:
- 蓋上去!把原始碼的 VCS revision 包含進你的程式中。
- 這不是什麼新點子:i3 的建置早在 2012 年就開始包含git-describe(1) 的 revision 了!
- 串接起來!在建置/打包時,確保 VCS revision 不會遺失。
- 上面「在 NixOS 上取得 VCS rev」的案例研究章節,就說明了 VCS revision 可能遺失的幾個原因、哪些路徑可行,以及如何修補缺失的串接。
- 回報出來!讓你的軟體在每一個相關的介面上都印出它的 VCS revision,例如:
- 可執行程式:在用
--version執行時回報 VCS revision- 對於 Go 程式,你永遠都可以用
go version -m
- 對於 Go 程式,你永遠都可以用
- 服務與批次工作:在啟動日誌中包含 VCS revision。
- 送出的 HTTP 請求:在
User-Agent中包含 VCS revision - HTTP 回應:在 header 中包含 VCS revision(對內)
- 遠端程序呼叫(RPC):在 RPC metadata 中包含 revision
- 使用者介面:在某個顯眼可見的地方暴露 revision 以供除錯。
- 可執行程式:在用
在整個系統中實作「版本可觀測性」是一個只需一天、高投資報酬率的專案。
透過我的 Nix 範例,你已經看到 VCS revision 如何在整個堆疊中都是可取得的,卻可能在中途遺失。希望我的資源也能幫助你快速修復你的技術堆疊:
- 我的
stapelberg/nix/go-vcs-stampingoverlay,適用於 Nix / NixOS - 我的
stampit儲存庫是一個收集範例(以 markdown 內容形式)的社群資源,其中包含一個 Go 模組與幾個小幫手,讓版本回報變得輕而易舉。
現在就去為你的程式和資料傳輸蓋上戳記吧!🚀
隨機一篇部落格
留言
登入後參與討論