From 0ec1439ddcbf28884421ed2f4062324c29800143 Mon Sep 17 00:00:00 2001 From: Victor Solano Date: Wed, 26 Aug 2026 11:52:41 +0200 Subject: [PATCH] feat: add eko version command with runtime and build metadata Adds an `eko version` subcommand and matching `-v` / `--version` root flags that report the CLI version, Go runtime version, target OS/arch, Git commit, and build date. - cmd/version.go: `versionCmd` plus the `Version`, `Commit` and `BuildDate` variables stamped via `-ldflags` at release time. A shared `formatVersion` helper renders the banner so the subcommand and the root flags cannot drift apart. - cmd/root.go: wires `Version` and a custom version template into the root command and registers the `-v` / `--version` flags. - .goreleaser.yaml: injects the release version, commit and date through the Go linker so published archives report real metadata. - Build metadata falls back to `dev` / `unknown` when it is not stamped (and when a linker flag injects an empty string), so a plain `go build` never prints blank fields. - cmd/commands_test.go: covers the banner format, the empty-metadata fallback, the subcommand, argument rejection, and `-v` / `--version` parity with the subcommand. - Documents the command in README.md and docs/docs/cli-reference.md. Closes #78 --- .goreleaser.yaml | 2 + README.md | 3 ++ cmd/commands_test.go | 83 ++++++++++++++++++++++++++++++++++++++ cmd/root.go | 11 ++++- cmd/version.go | 55 +++++++++++++++++++++++++ docs/docs/cli-reference.md | 37 +++++++++++++++++ 6 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 cmd/version.go diff --git a/.goreleaser.yaml b/.goreleaser.yaml index ba123e0..8a6cb9a 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -9,6 +9,8 @@ before: builds: - env: - CGO_ENABLED=1 + ldflags: + - -s -w -X eko/cmd.Version={{.Version}} -X eko/cmd.Commit={{.Commit}} -X eko/cmd.BuildDate={{.Date}} goos: - linux - windows diff --git a/README.md b/README.md index a5d23f1..19645d3 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,9 @@ go build -o eko main.go # Initialize Eko in any directory eko init +# Inspect the installed version and build metadata +eko version + # Save an instant local snapshot (with optional AI summary) eko save -m "Refactored payment gateway handler" --ai diff --git a/cmd/commands_test.go b/cmd/commands_test.go index 9f5d17b..cad94ee 100644 --- a/cmd/commands_test.go +++ b/cmd/commands_test.go @@ -1919,3 +1919,86 @@ func TestCompletionCmd_TooManyArgs(t *testing.T) { t.Error("expected error when more than one shell argument is given, got nil") } } + +func TestFormatVersion(t *testing.T) { + got := formatVersion("1.1.0", "8c9d1a2f", "2026-08-26T08:00:00Z") + want := "eko version 1.1.0 (" + runtime.GOOS + "/" + runtime.GOARCH + ")\n" + + "Go version: " + runtime.Version() + "\n" + + "Git commit: 8c9d1a2f\n" + + "Build date: 2026-08-26T08:00:00Z\n" + + if got != want { + t.Fatalf("unexpected version banner:\nwant:\n%s\ngot:\n%s", want, got) + } +} + +func TestFormatVersion_blankBuildMetadataFallsBackToPlaceholders(t *testing.T) { + // A build without -ldflags (or with empty values injected) must never print + // blank fields. + got := formatVersion("", "", " ") + want := "eko version dev (" + runtime.GOOS + "/" + runtime.GOARCH + ")\n" + + "Go version: " + runtime.Version() + "\n" + + "Git commit: unknown\n" + + "Build date: unknown\n" + + if got != want { + t.Fatalf("unexpected fallback version banner:\nwant:\n%s\ngot:\n%s", want, got) + } +} + +func TestVersionCommand_defaultBuildMetadata(t *testing.T) { + // The package defaults are what an un-stamped `go build` produces. + if Version != "dev" || Commit != "unknown" || BuildDate != "unknown" { + t.Fatalf("unexpected default build metadata: version=%q commit=%q buildDate=%q", Version, Commit, BuildDate) + } +} + +func TestVersionCommand(t *testing.T) { + got, err := executeCommand(rootCmd, "version") + if err != nil { + t.Fatalf("eko version returned an error: %v", err) + } + + want := formatVersion(Version, Commit, BuildDate) + if got != want { + t.Fatalf("unexpected eko version output:\nwant:\n%s\ngot:\n%s", want, got) + } + + for _, fragment := range []string{ + runtime.Version(), + runtime.GOOS + "/" + runtime.GOARCH, + "Git commit:", + } { + if !strings.Contains(got, fragment) { + t.Errorf("expected version output to contain %q, got:\n%s", fragment, got) + } + } +} + +func TestVersionCommand_rejectsArguments(t *testing.T) { + if _, err := executeCommand(rootCmd, "version", "extra"); err == nil { + t.Error("expected an error when eko version is given a positional argument, got nil") + } +} + +func TestVersionFlags_matchVersionSubcommand(t *testing.T) { + want := formatVersion(Version, Commit, BuildDate) + + for _, flag := range []string{"--version", "-v"} { + t.Run(flag, func(t *testing.T) { + t.Cleanup(func() { + if err := rootCmd.Flags().Set("version", "false"); err != nil { + t.Errorf("reset version flag: %v", err) + } + }) + + got, err := executeCommand(rootCmd, flag) + if err != nil { + t.Fatalf("eko %s returned an error: %v", flag, err) + } + if got != want { + t.Fatalf("unexpected eko %s output:\nwant:\n%s\ngot:\n%s", flag, want, got) + } + }) + } +} diff --git a/cmd/root.go b/cmd/root.go index 2b91f30..b9efc63 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -12,13 +12,20 @@ import ( ) var rootCmd = &cobra.Command{ - Use: "eko", - Short: "eko – AI Snapshot Versioning CLI", + Use: "eko", + Short: "eko – AI Snapshot Versioning CLI", + Version: Version, SilenceUsage: true, SilenceErrors: true, } +func init() { + // `eko -v` / `eko --version` print the same banner as `eko version`. + rootCmd.SetVersionTemplate(formatVersion(Version, Commit, BuildDate)) + rootCmd.Flags().BoolP("version", "v", false, "print version, runtime and build information") +} + func Execute() { shutdown, err := telemetry.Init(context.Background()) if err != nil { diff --git a/cmd/version.go b/cmd/version.go new file mode 100644 index 0000000..216350f --- /dev/null +++ b/cmd/version.go @@ -0,0 +1,55 @@ +package cmd + +import ( + "fmt" + "runtime" + "strings" + + "github.com/spf13/cobra" +) + +// Build metadata. These are overridden at release time through the Go linker, +// e.g. -ldflags "-X eko/cmd.Version=1.1.0 -X eko/cmd.Commit=8c9d1a2f". +// The defaults keep locally built binaries readable instead of printing blanks. +var ( + Version = "dev" + Commit = "unknown" + BuildDate = "unknown" +) + +var versionCmd = &cobra.Command{ + Use: "version", + Short: "Print version, runtime and build information", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + _, err := fmt.Fprint(cmd.OutOrStdout(), formatVersion(Version, Commit, BuildDate)) + return err + }, +} + +// formatVersion renders the version banner shared by `eko version` and the +// root-level -v/--version flags. +func formatVersion(version, commit, buildDate string) string { + return fmt.Sprintf( + "eko version %s (%s/%s)\nGo version: %s\nGit commit: %s\nBuild date: %s\n", + orFallback(version, "dev"), + runtime.GOOS, + runtime.GOARCH, + runtime.Version(), + orFallback(commit, "unknown"), + orFallback(buildDate, "unknown"), + ) +} + +// orFallback guards against linker flags that inject empty strings, so the +// banner never reports a blank field. +func orFallback(value, fallback string) string { + if strings.TrimSpace(value) == "" { + return fallback + } + return value +} + +func init() { + rootCmd.AddCommand(versionCmd) +} diff --git a/docs/docs/cli-reference.md b/docs/docs/cli-reference.md index af07a87..7ffcea4 100644 --- a/docs/docs/cli-reference.md +++ b/docs/docs/cli-reference.md @@ -32,6 +32,7 @@ Exhaustive reference guide for all commands, flags, options, and aliases in the | [`eko ai next`](#10-eko-ai-next) | None | AI task & issue recommendation engine | None | | [`eko ai security`](#10-eko-ai-security) | None | AI hardcoded secret & vulnerability scanner | None | | [`eko ai gate`](#10-eko-ai-gate) | None | AI pre-commit quality gate evaluation | None | +| [`eko version`](#11-eko-version) | None | Print CLI version, Go runtime, OS/arch, git commit | `-v`/`--version` on the root command | --- @@ -314,6 +315,42 @@ eko ai gate --- +## 11. `eko version` + +Prints the Eko release version, target operating system and architecture, Go runtime version, Git commit, and build date. The root-level `-v` and `--version` flags produce the exact same output. + +```bash +eko version +eko --version +eko -v +``` + +```text +eko version 1.1.0 (darwin/arm64) +Go version: go1.26.7 +Git commit: 8c9d1a2f +Build date: 2026-08-26T08:22:00Z +``` + +### Build Metadata + +Release archives are built by GoReleaser, which stamps the values through the Go linker: + +```bash +go build -ldflags "-X eko/cmd.Version=1.1.0 -X eko/cmd.Commit=8c9d1a2f -X eko/cmd.BuildDate=2026-08-26T08:22:00Z" -o eko . +``` + +A plain `go build` leaves those variables at their defaults, so locally built binaries report `dev` and `unknown` instead of blank fields: + +```text +eko version dev (darwin/arm64) +Go version: go1.26.7 +Git commit: unknown +Build date: unknown +``` + +--- + ## Global Environment Variables | Variable | Default Value | Usage |