From 03fd060a157db31f82c14c0148ba4af1ef274840 Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Wed, 3 Jun 2026 13:06:16 +0800 Subject: [PATCH] fix(version): report module version for go-install builds Binaries installed with 'go install github.com/api7/a6/cmd/a6@' bypass the Makefile/GoReleaser ldflags, so 'a6 version' reported 'dev' with unknown commit and date. Fall back to runtime/debug.ReadBuildInfo: the Go module version fills Version, and vcs.revision/vcs.time fill Commit/Date for source builds. Values injected via ldflags still take precedence. This also fixes 'a6 update' treating go-installed binaries as dev builds when comparing against the latest release. Audited for the same bug class: internal/version is the only ldflags-injected metadata in the repo. --- docs/user-guide/version.md | 6 +-- internal/version/version.go | 41 +++++++++++++++++++ internal/version/version_test.go | 68 ++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 internal/version/version_test.go diff --git a/docs/user-guide/version.md b/docs/user-guide/version.md index 9c702e5..7c0d0fd 100644 --- a/docs/user-guide/version.md +++ b/docs/user-guide/version.md @@ -16,9 +16,9 @@ a6 version v1.2.3 | Field | Source | |---|---| -| `version` | Tagged release if built from a tag (e.g. `v0.1.0-rc1`); otherwise the short commit. `dev` when built without ldflags. | -| `commit` | Short Git commit the binary was built from. | -| `built` | UTC timestamp of the build. | +| `version` | Release tag injected at build time (`make build`, GoReleaser), or the Go module version for binaries installed with `go install github.com/api7/a6/cmd/a6@`. `dev` only for untagged local source builds. | +| `commit` | Short Git commit the binary was built from (build-time injection, or VCS info embedded by `go build`). `unknown` when neither is available, e.g. `go install` module builds. | +| `built` | UTC timestamp of the build (same sources as `commit`). | | `go` | Go toolchain used. | | `platform` | `/` of the binary. | diff --git a/internal/version/version.go b/internal/version/version.go index 1804c5f..5ee4d1a 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -1,5 +1,7 @@ package version +import "runtime/debug" + // These variables are set at build time via ldflags. var ( // Version is the semantic version (e.g., "v0.1.0"). @@ -9,3 +11,42 @@ var ( // Date is the build date in UTC. Date = "unknown" ) + +// init falls back to the Go module build info for any value that was not +// injected via ldflags, so that binaries installed with +// `go install github.com/api7/a6/cmd/a6@` report the module version +// instead of "dev". +func init() { + if info, ok := debug.ReadBuildInfo(); ok { + Version, Commit, Date = resolve(Version, Commit, Date, info) + } +} + +// resolve returns version, commit, and date with unset (default) values +// filled in from build info. Values already set via ldflags take precedence. +func resolve(version, commit, date string, info *debug.BuildInfo) (string, string, string) { + if version == "dev" && info.Main.Version != "" && info.Main.Version != "(devel)" { + version = info.Main.Version + } + + var revision, vcsTime string + for _, s := range info.Settings { + switch s.Key { + case "vcs.revision": + revision = s.Value + case "vcs.time": + vcsTime = s.Value + } + } + if commit == "unknown" && revision != "" { + if len(revision) > 7 { + revision = revision[:7] + } + commit = revision + } + if date == "unknown" && vcsTime != "" { + date = vcsTime + } + + return version, commit, date +} diff --git a/internal/version/version_test.go b/internal/version/version_test.go new file mode 100644 index 0000000..c701309 --- /dev/null +++ b/internal/version/version_test.go @@ -0,0 +1,68 @@ +package version + +import ( + "runtime/debug" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestResolve_GoInstallModuleVersion(t *testing.T) { + // `go install .../cmd/a6@v1.0.0` embeds the module version but no ldflags. + info := &debug.BuildInfo{Main: debug.Module{Version: "v1.0.0"}} + + v, c, d := resolve("dev", "unknown", "unknown", info) + + assert.Equal(t, "v1.0.0", v) + assert.Equal(t, "unknown", c) + assert.Equal(t, "unknown", d) +} + +func TestResolve_DevelBuildStaysDev(t *testing.T) { + // `go build` from a source checkout reports "(devel)" as the module version. + info := &debug.BuildInfo{Main: debug.Module{Version: "(devel)"}} + + v, _, _ := resolve("dev", "unknown", "unknown", info) + + assert.Equal(t, "dev", v) +} + +func TestResolve_LdflagsTakePrecedence(t *testing.T) { + info := &debug.BuildInfo{ + Main: debug.Module{Version: "v9.9.9"}, + Settings: []debug.BuildSetting{ + {Key: "vcs.revision", Value: "ffffffffffffffffffffffffffffffffffffffff"}, + {Key: "vcs.time", Value: "2026-01-01T00:00:00Z"}, + }, + } + + v, c, d := resolve("v1.2.3", "abc1234", "2026-05-27T03:23:51Z", info) + + assert.Equal(t, "v1.2.3", v) + assert.Equal(t, "abc1234", c) + assert.Equal(t, "2026-05-27T03:23:51Z", d) +} + +func TestResolve_VCSSettingsFallback(t *testing.T) { + info := &debug.BuildInfo{ + Main: debug.Module{Version: "(devel)"}, + Settings: []debug.BuildSetting{ + {Key: "vcs.revision", Value: "0123456789abcdef0123456789abcdef01234567"}, + {Key: "vcs.time", Value: "2026-06-03T00:00:00Z"}, + }, + } + + v, c, d := resolve("dev", "unknown", "unknown", info) + + assert.Equal(t, "dev", v) + assert.Equal(t, "0123456", c, "commit should be shortened to 7 characters") + assert.Equal(t, "2026-06-03T00:00:00Z", d) +} + +func TestResolve_EmptyBuildInfo(t *testing.T) { + v, c, d := resolve("dev", "unknown", "unknown", &debug.BuildInfo{}) + + assert.Equal(t, "dev", v) + assert.Equal(t, "unknown", c) + assert.Equal(t, "unknown", d) +}