diff --git a/README.md b/README.md index 5db6a14..1fc822c 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ synthetic secrets) ships in [`examples/`](examples/README.md). ```sh # from a released tag (binaries also attach to each GitHub Release): -go install github.com/Kelcode-Dev/proxops/cmd/proxops@v0.6.0 +go install github.com/Kelcode-Dev/proxops/cmd/proxops@v0.6.1 # or build from source: git clone https://github.com/Kelcode-Dev/proxops && cd proxops @@ -107,7 +107,7 @@ make build # -> bin/proxops Versioned releases follow SemVer tags (`vX.Y.Z`); `@main` installs the current main-branch build instead. For PVE hosts without a Go toolchain, each GitHub Release also carries static `proxops_vX.Y.Z__` -binaries (`make cross` produces the linux/amd64 one). See +binaries (`make cross` produces both Linux amd64 and arm64 binaries). See `docs/getting-started.md`. SOPS-backed credentials additionally require `sops` (+ `age`) on PATH. diff --git a/cmd/proxops/main.go b/cmd/proxops/main.go index 96fdd91..b6c9360 100644 --- a/cmd/proxops/main.go +++ b/cmd/proxops/main.go @@ -43,6 +43,7 @@ import ( "os" "os/signal" "path/filepath" + "runtime/debug" "syscall" "time" @@ -55,9 +56,32 @@ import ( "github.com/Kelcode-Dev/proxops/internal/metrics" ) -// version is set at build time via -ldflags. +// version is overridden at build time via -ldflags. Module-aware builds +// fall back to the version embedded by the Go toolchain. var version = "dev" +func resolveVersion(linked, module string) string { + if linked != "dev" { + return linked + } + + if module != "" && module != "(devel)" { + return module + } + + return linked +} + +func resolvedVersion() string { + moduleVersion := "" + + if info, ok := debug.ReadBuildInfo(); ok { + moduleVersion = info.Main.Version + } + + return resolveVersion(version, moduleVersion) +} + type globalFlags struct { configPath string logLevel string @@ -98,6 +122,7 @@ var errSilent = errors.New("silent") func newRootCmd() *cobra.Command { var gf globalFlags + v := resolvedVersion() root := &cobra.Command{ Use: "proxops", @@ -125,7 +150,7 @@ func newRootCmd() *cobra.Command { "(a proxops.yaml with git.url) and single-cluster configs.\n" + "diff/apply/status/run process every discovered cluster in sorted\n" + "name order; adopt requires an explicit --cluster=.\n", - Version: version, + Version: v, SilenceUsage: true, SilenceErrors: true, } @@ -268,7 +293,7 @@ func buildAgent(gf *globalFlags, log *slog.Logger, persistent, local *pflag.Flag // Validate() with empty SopsResolved, causing SOPS configs that have // no global pve.user/token triple (which is correct — SOPS supplies // them) to fail validation prematurely. - return app.New(cfg, log, metrics.Register(), version) + return app.New(cfg, log, metrics.Register(), resolvedVersion()) } // newRunCmd is the daemon mode: poll git and reconcile PVE continuously, diff --git a/cmd/proxops/main_test.go b/cmd/proxops/main_test.go index 37f2903..d25e2e3 100644 --- a/cmd/proxops/main_test.go +++ b/cmd/proxops/main_test.go @@ -309,3 +309,61 @@ func TestBuildAgentRepositoryFirst(t *testing.T) { t.Fatalf("agent build must fail at the git stage, got: %v", aerr) } } + +// TestResolveVersion pins version precedence across release builds, +// `go install @version`, and local development builds. An explicit +// linker-supplied version always wins; otherwise the Go module version +// is used when available, falling back to "dev". +func TestResolveVersion(t *testing.T) { + tests := []struct { + name string + linked string + module string + want string + }{ + { + name: "release linker version wins", + linked: "v0.6.1", + module: "v0.6.0", + want: "v0.6.1", + }, + { + name: "git-derived linker version wins", + linked: "v0.6.0-3-gabc1234", + module: "(devel)", + want: "v0.6.0-3-gabc1234", + }, + { + name: "go install uses module version", + linked: "dev", + module: "v0.6.1", + want: "v0.6.1", + }, + { + name: "development build remains dev", + linked: "dev", + module: "(devel)", + want: "dev", + }, + { + name: "missing build info remains dev", + linked: "dev", + module: "", + want: "dev", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := resolveVersion(tt.linked, tt.module); got != tt.want { + t.Fatalf( + "resolveVersion(%q, %q) = %q, want %q", + tt.linked, + tt.module, + got, + tt.want, + ) + } + }) + } +}