Skip to content

Commit a660691

Browse files
committed
fix: self-report version from build info (correct for go install)
The version was only stamped via ldflags (GoReleaser / make), so `go install` builds reported the hardcoded dev default. Resolve it with precedence ldflags > embedded module version (go install …@vX.Y.Z) > VCS revision > dev, via runtime/debug.ReadBuildInfo. Covered by a table test. Document the rule that published tags must never be moved (the module proxy caches them immutably).
1 parent b10e522 commit a660691

3 files changed

Lines changed: 86 additions & 3 deletions

File tree

CONTRIBUTING.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,14 @@ goreleaser check # validate .goreleaser.yaml
6262
goreleaser release --snapshot --clean # full build into ./dist
6363
```
6464

65+
> ⚠️ **Never move or re-point a published tag.** The Go module proxy caches every
66+
> `vX.Y.Z` immutably, so moving a tag silently breaks `go install` even when
67+
> GitHub shows the new commit. Always bump to a fresh version instead.
68+
69+
The binary's version is resolved automatically (`internal/cli.resolveVersion`):
70+
ldflags for release / `make` builds, the embedded module version for
71+
`go install …@vX.Y.Z`, and a `dev-<rev>` string for plain `go build` — so no
72+
build mode reports a stale version.
73+
6574
CI (`.github/workflows/ci.yml`) runs gofmt, vet, build, and tests on every push
6675
and pull request.

internal/cli/cli.go

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"math"
88
"os"
99
"os/exec"
10+
"runtime/debug"
1011
"time"
1112

1213
"github.com/diomonogatari/hydrate-cli/internal/config"
@@ -15,8 +16,49 @@ import (
1516
"github.com/diomonogatari/hydrate-cli/internal/store"
1617
)
1718

18-
// version is overridable at build time with -ldflags "-X ...cli.version=...".
19-
var version = "0.1.0-dev"
19+
// version is set at build time via -ldflags "-X ...cli.version=..." by GoReleaser
20+
// and `make build`. When empty (e.g. a `go install [email protected]` build, which gets
21+
// no ldflags), resolveVersion falls back to the module/VCS info the Go toolchain
22+
// embeds — so released binaries always self-report a real version.
23+
var version = ""
24+
25+
// resolveVersion returns the best available version string for this build.
26+
func resolveVersion() string {
27+
bi, _ := debug.ReadBuildInfo()
28+
return versionFrom(version, bi)
29+
}
30+
31+
// versionFrom is the pure resolution logic, separated for testing. Precedence:
32+
// ldflags > module version (`go install module@v…`) > VCS revision > "dev".
33+
func versionFrom(ldflagsVer string, bi *debug.BuildInfo) string {
34+
if ldflagsVer != "" {
35+
return ldflagsVer
36+
}
37+
if bi == nil {
38+
return "dev"
39+
}
40+
if v := bi.Main.Version; v != "" && v != "(devel)" {
41+
return v
42+
}
43+
var rev, dirty string
44+
for _, s := range bi.Settings {
45+
switch s.Key {
46+
case "vcs.revision":
47+
rev = s.Value
48+
case "vcs.modified":
49+
if s.Value == "true" {
50+
dirty = "-dirty"
51+
}
52+
}
53+
}
54+
if rev != "" {
55+
if len(rev) > 12 {
56+
rev = rev[:12]
57+
}
58+
return "dev-" + rev + dirty
59+
}
60+
return "dev"
61+
}
2062

2163
// Run is the program entry point. It returns a process exit code.
2264
func Run(args []string) int {
@@ -30,7 +72,7 @@ func Run(args []string) int {
3072
usage(os.Stdout)
3173
return 0
3274
case "version", "-v", "--version":
33-
fmt.Println("hydrate", version)
75+
fmt.Println("hydrate", resolveVersion())
3476
return 0
3577
}
3678
}

internal/cli/cli_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cli
22

33
import (
44
"os"
5+
"runtime/debug"
56
"testing"
67
)
78

@@ -35,3 +36,34 @@ func TestRunTopLevelDispatch(t *testing.T) {
3536
}
3637
}
3738
}
39+
40+
func TestVersionFrom(t *testing.T) {
41+
mod := func(v string) *debug.BuildInfo { return &debug.BuildInfo{Main: debug.Module{Version: v}} }
42+
withVCS := func(rev, modified string) *debug.BuildInfo {
43+
return &debug.BuildInfo{
44+
Main: debug.Module{Version: "(devel)"},
45+
Settings: []debug.BuildSetting{
46+
{Key: "vcs.revision", Value: rev},
47+
{Key: "vcs.modified", Value: modified},
48+
},
49+
}
50+
}
51+
52+
cases := []struct {
53+
name string
54+
ldflags string
55+
bi *debug.BuildInfo
56+
want string
57+
}{
58+
{"ldflags wins", "1.0.2", mod("v9.9.9"), "1.0.2"},
59+
{"go install module version", "", mod("v1.0.2"), "v1.0.2"},
60+
{"devel falls to vcs (dirty)", "", withVCS("0123456789abcdef0", "true"), "dev-0123456789ab-dirty"},
61+
{"devel falls to vcs (clean)", "", withVCS("0123456789abcdef0", "false"), "dev-0123456789ab"},
62+
{"no build info", "", nil, "dev"},
63+
}
64+
for _, c := range cases {
65+
if got := versionFrom(c.ldflags, c.bi); got != c.want {
66+
t.Errorf("%s: versionFrom(%q, …) = %q, want %q", c.name, c.ldflags, got, c.want)
67+
}
68+
}
69+
}

0 commit comments

Comments
 (0)