diff --git a/internal/version/version.go b/internal/version/version.go index 1804c5f..d7eeae6 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/a7/cmd/a7@` 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..e645063 --- /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/a7@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) +}