Stamp shared build metadata into banger binaries

Treat `banger`, `bangerd`, and `banger-vsock-agent` as one release by
stamping the same version, commit SHA, and build timestamp into every
binary through a shared ldflag-backed `internal/buildinfo` package.

Add `banger version`, extend daemon ping/status to report the running
daemon's build tuple, and keep the guest helper linked to the same build
metadata without adding a new public version surface for it.

Validate with `GOCACHE=/tmp/banger-gocache go test ./...`, `make build`,
`./build/bin/banger version`, and `./build/bin/banger daemon status`
after the daemon restarts onto the new binary.
This commit is contained in:
Thales Maciel 2026-03-22 17:14:06 -03:00
parent ea2db1e868
commit f798e1db33
No known key found for this signature in database
GPG key ID: 33112E6833C34679
9 changed files with 219 additions and 13 deletions

View file

@ -15,6 +15,7 @@ import (
"time"
"banger/internal/api"
"banger/internal/buildinfo"
"banger/internal/model"
"banger/internal/system"
)
@ -25,12 +26,36 @@ func TestNewBangerCommandHasExpectedSubcommands(t *testing.T) {
for _, sub := range cmd.Commands() {
names = append(names, sub.Name())
}
want := []string{"daemon", "doctor", "image", "internal", "vm"}
want := []string{"daemon", "doctor", "image", "internal", "version", "vm"}
if !reflect.DeepEqual(names, want) {
t.Fatalf("subcommands = %v, want %v", names, want)
}
}
func TestVersionCommandPrintsBuildInfo(t *testing.T) {
cmd := NewBangerCommand()
var stdout bytes.Buffer
cmd.SetOut(&stdout)
cmd.SetErr(&stdout)
cmd.SetArgs([]string{"version"})
if err := cmd.Execute(); err != nil {
t.Fatalf("Execute: %v", err)
}
info := buildinfo.Current()
output := stdout.String()
for _, want := range []string{
"version: " + info.Version,
"commit: " + info.Commit,
"built_at: " + info.BuiltAt,
} {
if !strings.Contains(output, want) {
t.Fatalf("output = %q, want %q", output, want)
}
}
}
func TestLegacyRemovedCommandIsRejected(t *testing.T) {
cmd := NewBangerCommand()
cmd.SetArgs([]string{"tui"})
@ -1222,6 +1247,55 @@ func TestDaemonStatusIncludesLogPathWhenStopped(t *testing.T) {
}
}
func TestDaemonStatusIncludesDaemonBuildInfoWhenRunning(t *testing.T) {
origDaemonPing := daemonPingFunc
t.Cleanup(func() {
daemonPingFunc = origDaemonPing
})
configHome := filepath.Join(t.TempDir(), "config")
stateHome := filepath.Join(t.TempDir(), "state")
runtimeHome := filepath.Join(t.TempDir(), "runtime")
t.Setenv("XDG_CONFIG_HOME", configHome)
t.Setenv("XDG_STATE_HOME", stateHome)
t.Setenv("XDG_RUNTIME_DIR", runtimeHome)
daemonPingFunc = func(context.Context, string) (api.PingResult, error) {
return api.PingResult{
Status: "ok",
PID: 42,
WebURL: "http://127.0.0.1:7777",
Version: "v1.2.3",
Commit: "abc123",
BuiltAt: "2026-03-22T12:00:00Z",
}, nil
}
cmd := NewBangerCommand()
var stdout bytes.Buffer
cmd.SetOut(&stdout)
cmd.SetErr(&stdout)
cmd.SetArgs([]string{"daemon", "status"})
if err := cmd.Execute(); err != nil {
t.Fatalf("Execute: %v", err)
}
output := stdout.String()
for _, want := range []string{
"running\n",
"pid: 42",
"version: v1.2.3",
"commit: abc123",
"built_at: 2026-03-22T12:00:00Z",
"log: " + filepath.Join(stateHome, "banger", "bangerd.log"),
"web: http://127.0.0.1:7777",
} {
if !strings.Contains(output, want) {
t.Fatalf("output = %q, want %q", output, want)
}
}
}
func TestBuildDaemonCommandIsDetachedFromCallerContext(t *testing.T) {
cmd := buildDaemonCommand("/tmp/bangerd")