diff --git a/internal/version/version.go b/internal/version/version.go index dbc06d2..ea98bfb 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -11,16 +11,42 @@ var Version = "0.0.0-dev" // devVersion is the source default, present when no release version was stamped in. const devVersion = "0.0.0-dev" +// released returns the release version this binary carries, or "" when it is an +// unstamped development build. A goreleaser build sets Version through ldflags; a +// `go install @` build does not, but Go records the module version in +// the build info, so that is used as the fallback. "(devel)" is Go's placeholder for a +// build from a local checkout and is treated as unversioned. +func released() string { + if Version != devVersion { + return Version + } + if bi, ok := debug.ReadBuildInfo(); ok { + return moduleVersion(bi.Main.Version) + } + return "" +} + +// moduleVersion returns v when it is a usable module version, or "" for an absent +// version or Go's "(devel)" placeholder (a build from a local checkout). +func moduleVersion(v string) string { + if v == "" || v == "(devel)" { + return "" + } + return v +} + // IsDev reports whether this is an unstamped development build (no release version linked // in). A dev build keeps its durable state apart from a release build's, so an -// in-progress schema change on a branch never touches a real installation. -func IsDev() bool { return Version == devVersion } +// in-progress schema change on a branch never touches a real installation. A build +// installed with `go install ...@` is a real release, not a dev build. +func IsDev() bool { return released() == "" } -// String returns a human-readable version, appending the VCS revision when the -// binary was built from a git checkout and no explicit version was set. +// String returns a human-readable version: the release version when one is present, +// otherwise the source default with the VCS revision appended when the binary was built +// from a git checkout. func String() string { - if Version != devVersion { - return Version + if v := released(); v != "" { + return v } if bi, ok := debug.ReadBuildInfo(); ok { for _, s := range bi.Settings { diff --git a/internal/version/version_test.go b/internal/version/version_test.go index 7061975..175456d 100644 --- a/internal/version/version_test.go +++ b/internal/version/version_test.go @@ -13,3 +13,33 @@ func TestIsDevOnUnstampedBuild(t *testing.T) { t.Fatal("IsDev() = false for an unstamped build, want true") } } + +// TestStampedVersionWins confirms a release version linked in through ldflags is reported +// verbatim and is not treated as a development build. +func TestStampedVersionWins(t *testing.T) { + old := Version + defer func() { Version = old }() + Version = "v9.9.9" + if got := String(); got != "v9.9.9" { + t.Fatalf("String() = %q, want v9.9.9", got) + } + if IsDev() { + t.Fatal("IsDev() = true for a stamped build, want false") + } +} + +// TestModuleVersionFallback confirms the `go install @` path: when no +// release version is stamped through ldflags, a real module version from the build info +// is used, while an absent version or Go's "(devel)" placeholder is not. +func TestModuleVersionFallback(t *testing.T) { + for in, want := range map[string]string{ + "v0.1.1": "v0.1.1", + "v1.2.3-rc1": "v1.2.3-rc1", + "(devel)": "", + "": "", + } { + if got := moduleVersion(in); got != want { + t.Errorf("moduleVersion(%q) = %q, want %q", in, got, want) + } + } +}