From 7b67cf874e2b39a3455c3620f566496799ec285b Mon Sep 17 00:00:00 2001 From: Ezequiel Canul Date: Fri, 2 Oct 2026 17:24:40 -0600 Subject: [PATCH] fix(cli): report the module version when no ldflag stamped one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `go install` cannot receive a maintainer's -ldflags: it compiles from the module proxy with its own flags, so `main.version` kept its "dev" default and `skill-sync --version` reported "dev" for every go-installed binary. The version was never missing, only unsurfaced. The toolchain already embeds it: $ go version -m $(which skill-sync) mod github.com/eezzekl/skill-sync v1.1.0 $ skill-sync --version skill-sync version dev resolveVersion now prefers the ldflag value, which GoReleaser stamps for the Homebrew, Scoop and tarball binaries, and otherwise falls back to the module version from runtime/debug.ReadBuildInfo. No functional impact: the version string only feeds Cobra's Version field and reaches no file the tool writes. This is a diagnostics fix, so that a bug report identifies which build it came from. Side effect worth noting: the toolchain stamps VCS state by default, so a plain local build now reports a pseudo-version such as v1.1.1-0.20261002231447-f9c2b733d696+dirty instead of "dev". That is kept deliberately — it pins the exact commit and flags a dirty tree. Builds with -buildvcs=false still report "dev". --- cmd/skill-sync/main.go | 34 ++++++++++++++- cmd/skill-sync/version_test.go | 75 ++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 cmd/skill-sync/version_test.go diff --git a/cmd/skill-sync/main.go b/cmd/skill-sync/main.go index e240664..c4d6681 100644 --- a/cmd/skill-sync/main.go +++ b/cmd/skill-sync/main.go @@ -2,15 +2,45 @@ package main import ( "os" + "runtime/debug" "github.com/eezzekl/skill-sync/internal/cli" ) +// devVersion is the placeholder reported when no build stamped a real version. +const devVersion = "dev" + // version is set at build time via -ldflags "-X main.version=vX.Y.Z" -var version = "dev" +var version = devVersion + +// resolveVersion decides which version string to report. A GoReleaser build +// stamps ldflagVersion and wins outright. A `go install` build cannot receive +// those ldflags, so it falls back to the module version the toolchain embeds. +// A plain local build has neither and reports devVersion. +func resolveVersion(ldflagVersion, moduleVersion string) string { + if ldflagVersion != "" && ldflagVersion != devVersion { + return ldflagVersion + } + // "(devel)" is what the toolchain reports for a build outside a tagged + // module version, which carries no more information than devVersion. + if moduleVersion != "" && moduleVersion != "(devel)" { + return moduleVersion + } + return devVersion +} + +// moduleVersion returns the module version the Go toolchain embeds in the +// binary, or an empty string when build information is unavailable. +func moduleVersion() string { + info, ok := debug.ReadBuildInfo() + if !ok { + return "" + } + return info.Main.Version +} func main() { - if err := cli.NewRootCmd(version).Execute(); err != nil { + if err := cli.NewRootCmd(resolveVersion(version, moduleVersion())).Execute(); err != nil { os.Exit(1) } } diff --git a/cmd/skill-sync/version_test.go b/cmd/skill-sync/version_test.go new file mode 100644 index 0000000..f1bc558 --- /dev/null +++ b/cmd/skill-sync/version_test.go @@ -0,0 +1,75 @@ +package main + +import "testing" + +// TestResolveVersion covers both ways this binary gets built. GoReleaser stamps +// the version through -ldflags, but `go install` cannot receive a maintainer's +// ldflags: it compiles from the module proxy with its own flags. In that path the +// only version available is the one the Go toolchain embeds in the binary, which +// is why the ldflag value alone is not enough. +func TestResolveVersion(t *testing.T) { + tests := []struct { + name string + ldflagVersion string + moduleVersion string + want string + }{ + { + name: "ldflag wins when GoReleaser stamped it", + ldflagVersion: "1.1.0", + moduleVersion: "v1.1.0+dirty", + want: "1.1.0", + }, + { + name: "falls back to module version on go install", + ldflagVersion: devVersion, + moduleVersion: "v1.1.0", + want: "v1.1.0", + }, + { + // The toolchain stamps VCS state by default, so a local build in a + // git worktree yields a pseudo-version rather than "(devel)". It is + // reported as-is on purpose: it pins the exact commit and flags a + // dirty tree, which is precisely what a bug report needs. + name: "reports the pseudo-version of a local VCS-stamped build", + ldflagVersion: devVersion, + moduleVersion: "v1.1.1-0.20261002231447-f9c2b733d696+dirty", + want: "v1.1.1-0.20261002231447-f9c2b733d696+dirty", + }, + { + // Reachable with -buildvcs=false. + name: "stays dev for a build with no VCS stamping", + ldflagVersion: devVersion, + moduleVersion: "(devel)", + want: devVersion, + }, + { + name: "stays dev when build info is unavailable", + ldflagVersion: devVersion, + moduleVersion: "", + want: devVersion, + }, + { + name: "falls back when the ldflag is stamped empty", + ldflagVersion: "", + moduleVersion: "v1.1.0", + want: "v1.1.0", + }, + { + name: "reports dev when neither source knows the version", + ldflagVersion: "", + moduleVersion: "", + want: devVersion, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := resolveVersion(tt.ldflagVersion, tt.moduleVersion) + if got != tt.want { + t.Errorf("resolveVersion(%q, %q) = %q, want %q", + tt.ldflagVersion, tt.moduleVersion, got, tt.want) + } + }) + } +}