Skip to content

Commit 8f3084b

Browse files
authored
feat(cli): add version subcommand and --version/-v flags (#7)
* feat(cli): add version subcommand and --version/-v flags Both nebula-mgmt and nebula-agent now accept `version`, `--version`, and `-v` and print a single-line banner: nebula-mgmt v0.1.2 (1b0d854, built 2026-05-11T13:22:49Z) When built without ldflags (e.g. `go install ...@latest`), missing fields are filled from runtime/debug.ReadBuildInfo. ldflag targets renamed main.version -> main.versionStr to avoid collision with the new internal/version package import. Closes #1 * fix(version): explicitly discard fmt.Fprintf return to satisfy errcheck
1 parent 1b0d854 commit 8f3084b

7 files changed

Lines changed: 166 additions & 5 deletions

File tree

.goreleaser.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ builds:
1616
flags: [-trimpath]
1717
ldflags:
1818
- -s -w
19-
- -X main.version={{.Version}}
19+
- -X main.versionStr={{.Version}}
2020
- -X main.commit={{.Commit}}
2121
- -X main.date={{.Date}}
2222

@@ -29,7 +29,7 @@ builds:
2929
flags: [-trimpath]
3030
ldflags:
3131
- -s -w
32-
- -X main.version={{.Version}}
32+
- -X main.versionStr={{.Version}}
3333
- -X main.commit={{.Commit}}
3434
- -X main.date={{.Date}}
3535

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ COPY . .
1414

1515
ARG VERSION=dev
1616
RUN CGO_ENABLED=0 go build -trimpath \
17-
-ldflags "-s -w -X main.version=${VERSION}" \
17+
-ldflags "-s -w -X main.versionStr=${VERSION}" \
1818
-o /out/nebula-mgmt ./cmd/nebula-mgmt
1919

2020
FROM alpine:3.20

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ Requires Go 1.26+.
107107
make build # outputs bin/nebula-mgmt and bin/nebula-agent
108108
```
109109

110+
After install, `nebula-mgmt version` and `nebula-agent --version` print the build version, short commit, and build date.
111+
110112
## Quickstart
111113

112114
### 1. Run the server

cmd/nebula-agent/main.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ import (
1111

1212
"github.com/juev/nebula-mesh/internal/agent"
1313
"github.com/juev/nebula-mesh/internal/config"
14+
"github.com/juev/nebula-mesh/internal/version"
15+
)
16+
17+
// Populated at build time via -ldflags (see .goreleaser.yml).
18+
var (
19+
versionStr = "dev"
20+
commit = "none"
21+
date = "unknown"
1422
)
1523

1624
func main() {
@@ -23,7 +31,7 @@ func main() {
2331
func run() error {
2432
if len(os.Args) < 2 {
2533
fmt.Fprintln(os.Stderr, "usage: nebula-agent <command> [flags]")
26-
fmt.Fprintln(os.Stderr, "commands: enroll, run")
34+
fmt.Fprintln(os.Stderr, "commands: enroll, run, version")
2735
return fmt.Errorf("no command specified")
2836
}
2937

@@ -32,6 +40,9 @@ func run() error {
3240
return runEnroll(os.Args[2:])
3341
case "run":
3442
return runAgent(os.Args[2:])
43+
case "version", "--version", "-v":
44+
version.Print(os.Stdout, "nebula-agent", versionStr, commit, date)
45+
return nil
3546
default:
3647
return fmt.Errorf("unknown command: %s", os.Args[1])
3748
}

cmd/nebula-mgmt/main.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ import (
77
"strings"
88

99
"github.com/juev/nebula-mesh/internal/cli"
10+
"github.com/juev/nebula-mesh/internal/version"
11+
)
12+
13+
// Populated at build time via -ldflags (see .goreleaser.yml).
14+
var (
15+
versionStr = "dev"
16+
commit = "none"
17+
date = "unknown"
1018
)
1119

1220
func main() {
@@ -31,6 +39,9 @@ func run() error {
3139
return runHost(os.Args[2:])
3240
case "network":
3341
return runNetwork(os.Args[2:])
42+
case "version", "--version", "-v":
43+
version.Print(os.Stdout, "nebula-mgmt", versionStr, commit, date)
44+
return nil
3445
default:
3546
printUsage()
3647
return fmt.Errorf("unknown command: %s", os.Args[1])
@@ -39,7 +50,7 @@ func run() error {
3950

4051
func printUsage() {
4152
fmt.Fprintln(os.Stderr, "usage: nebula-mgmt <command> [flags]")
42-
fmt.Fprintln(os.Stderr, "commands: init, serve, host, network")
53+
fmt.Fprintln(os.Stderr, "commands: init, serve, host, network, version")
4354
}
4455

4556
func runInit(args []string) error {

internal/version/version.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Package version formats a CLI version banner.
2+
//
3+
// Each binary's main package declares its own ldflag-populated
4+
// `version`, `commit`, `date` variables and passes them to Print. When the
5+
// binary is built without ldflags (e.g. `go install`), missing fields are
6+
// filled from runtime/debug.ReadBuildInfo where possible.
7+
package version
8+
9+
import (
10+
"fmt"
11+
"io"
12+
"runtime/debug"
13+
)
14+
15+
const (
16+
devVersion = "dev"
17+
noCommit = "none"
18+
unknownDate = "unknown"
19+
shortCommitLen = 7
20+
)
21+
22+
// Print writes a single-line version banner of the form
23+
//
24+
// <name> <version> (<commit>, built <date>)
25+
//
26+
// Empty / placeholder fields are filled from runtime/debug.ReadBuildInfo
27+
// when available so that `go install ...@latest` builds also produce a
28+
// useful banner.
29+
func Print(w io.Writer, name, version, commit, date string) {
30+
v, c, d := Resolve(version, commit, date)
31+
if len(c) > shortCommitLen {
32+
c = c[:shortCommitLen]
33+
}
34+
_, _ = fmt.Fprintf(w, "%s %s (%s, built %s)\n", name, v, c, d)
35+
}
36+
37+
// Resolve returns the version triple, falling back to VCS info from the
38+
// embedded build info when the input fields are placeholders or empty.
39+
// Exported for testing.
40+
func Resolve(version, commit, date string) (string, string, string) {
41+
if isPlaceholder(version) || isPlaceholder(commit) || isPlaceholder(date) {
42+
if bi, ok := debug.ReadBuildInfo(); ok {
43+
if isPlaceholder(version) && bi.Main.Version != "" && bi.Main.Version != "(devel)" {
44+
version = bi.Main.Version
45+
}
46+
for _, s := range bi.Settings {
47+
switch s.Key {
48+
case "vcs.revision":
49+
if isPlaceholder(commit) {
50+
commit = s.Value
51+
}
52+
case "vcs.time":
53+
if isPlaceholder(date) {
54+
date = s.Value
55+
}
56+
}
57+
}
58+
}
59+
}
60+
if version == "" {
61+
version = devVersion
62+
}
63+
if commit == "" {
64+
commit = noCommit
65+
}
66+
if date == "" {
67+
date = unknownDate
68+
}
69+
return version, commit, date
70+
}
71+
72+
func isPlaceholder(s string) bool {
73+
switch s {
74+
case "", devVersion, noCommit, unknownDate:
75+
return true
76+
}
77+
return false
78+
}

internal/version/version_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package version
2+
3+
import (
4+
"bytes"
5+
"strings"
6+
"testing"
7+
)
8+
9+
func TestPrint_FullValues(t *testing.T) {
10+
var buf bytes.Buffer
11+
Print(&buf, "nebula-mgmt", "v1.2.3", "abcdef0123456", "2026-01-02T03:04:05Z")
12+
got := buf.String()
13+
want := "nebula-mgmt v1.2.3 (abcdef0, built 2026-01-02T03:04:05Z)\n"
14+
if got != want {
15+
t.Errorf("Print = %q, want %q", got, want)
16+
}
17+
}
18+
19+
func TestPrint_ShortCommitNotTruncated(t *testing.T) {
20+
var buf bytes.Buffer
21+
Print(&buf, "x", "v1", "abc123", "2026")
22+
got := buf.String()
23+
if !strings.Contains(got, "(abc123, built 2026)") {
24+
t.Errorf("short commit was truncated: %q", got)
25+
}
26+
}
27+
28+
func TestResolve_AllPlaceholdersFillsDefaults(t *testing.T) {
29+
// With no ldflags, an `go install` of the test binary may still populate
30+
// VCS info from the test runner's module — so we only assert the
31+
// fallback contract: non-empty values for every field.
32+
v, c, d := Resolve("", "", "")
33+
if v == "" || c == "" || d == "" {
34+
t.Errorf("Resolve filled empties incompletely: v=%q c=%q d=%q", v, c, d)
35+
}
36+
}
37+
38+
func TestResolve_KeepsExplicitValues(t *testing.T) {
39+
v, c, d := Resolve("v9.9.9", "deadbeef", "2030-01-01")
40+
if v != "v9.9.9" || c != "deadbeef" || d != "2030-01-01" {
41+
t.Errorf("Resolve overwrote explicit values: v=%q c=%q d=%q", v, c, d)
42+
}
43+
}
44+
45+
func TestIsPlaceholder(t *testing.T) {
46+
cases := map[string]bool{
47+
"": true,
48+
"dev": true,
49+
"none": true,
50+
"unknown": true,
51+
"v1.0.0": false,
52+
"abc123": false,
53+
}
54+
for in, want := range cases {
55+
if got := isPlaceholder(in); got != want {
56+
t.Errorf("isPlaceholder(%q) = %v, want %v", in, got, want)
57+
}
58+
}
59+
}

0 commit comments

Comments
 (0)