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) + } + }) + } +}