go-cli-version-flag-surface-binaries
Root cause: internal/version.Version/Commit were already ldflag-injected into all three binaries by the Makefile, and openapi-cli (cobra) already had --version. The gap was purely flag handling in the two stdlib-flag binaries — neither registered a version flag, so the injected data was unreachable.
Shared version package (internal/version/version.go):
var (
Version = "0.1.0-dev" // -X openapi/internal/version.Version=...
Commit = "none" // -X openapi/internal/version.Commit=...
)
// String returns the canonical CLI version string: v<Version>-<Commit>.
func String() string { return fmt.Sprintf("v%s-%s", Version, Commit) }
cmd/openapi-mcp/main.go — uses the global flag package. Register with flag.Bool at package level; check immediately after flag.Parse() and return before server start (the stdio loop blocks forever on stdin, so printing and continuing would hang the process):
var showVersion = flag.Bool("version", false, "print version and exit")
func main() {
flag.Parse()
if *showVersion {
fmt.Println(version.String())
return // MUST return before runServer(): stdio loop blocks on stdin
}
if err := runServer(); err != nil {
log.Fatal(err)
}
}
cmd/openapi-daemon/main.go — uses a custom FlagSet. Add BoolVar; check right after fs.Parse, before PID-file defaulting and daemon start. The local Version var is kept for controller status JSON:
var Version = version.Version // reported in controller status JSON
var (
versionFlag bool
pidFile string
logFile string
)
func newFlagSet() *flag.FlagSet {
fs := flag.NewFlagSet("openapi-daemon", flag.ContinueOnError)
fs.BoolVar(&versionFlag, "version", false, "print version and exit")
fs.StringVar(&pidFile, "pidfile", "", "path to PID file")
fs.StringVar(&logFile, "logfile", "", "path to log file")
return fs
}
func run(args []string) int {
fs := newFlagSet()
if err := fs.Parse(args); err != nil {
return 2 // usage error preserved
}
if versionFlag {
fmt.Println(version.String())
return 0 // early return BEFORE PID-file defaulting and daemon start
}
if pidFile == "" {
pidFile = defaultPIDFile // defaulted only after the --version check
}
if err := writePIDFile(pidFile); err != nil { ... }
return runDaemon()
}
openapi-cli was already correct; I pinned cobra's version template to version.String() + "\n" so all three binaries are byte-identical for --version (the live-probe requirement).
Makefile — unchanged in principle (ldflags already injected into all three):
VERSION ?= 0.1.0
COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo none)
LDFLAGS := -X openapi/internal/version.Version=$(VERSION) \
-X openapi/internal/version.Commit=$(COMMIT)
openapi-mcp:
go build -ldflags "$(LDFLAGS)" -o bin/openapi-mcp ./cmd/openapi-mcp
**Tests (`version_test.go` per package, 7 passing, `go vet` clean, `gofmt` clean):**
| Test | Asserts |
|---|---|
| mcp `TestVersionFlagRegistered` | flag on global `flag.CommandLine`, default `false` |
| mcp `TestVersionFlagPrintsVersion` | subprocess runs real `main()` with `--version`: rc=0, stdout contains `version.String()`, **no `serving on stdio` marker on stderr**, completes within 5s timeout (no stdin hang) |
| daemon `TestVersionFlagRegistered` | `--version` on the custom `FlagSet` |
| daemon `TestVersionFlagPrintsVersion` | rc=0, stdout == `version.String()` |
| daemon `TestVersionFlagSkipsPIDFileDefaulting` | **early return before defaulting**: default PID path (redirected to temp dir) is never created |
| daemon `TestVersionFlagWithPidfileArg` | `--pidfile X --version` returns 0 and never writes X |
| daemon `TestVersionVarKeptForStatusJSON` | local `Version` == `internal/version.Version`, wired into controller status payload |
**Live probe** (built via `make`, commit `70ac5a6`):
```
$ make probe
v0.1.0-70ac5a6 # openapi-cli
v0.1.0-70ac5a6 # openapi-mcp
v0.1.0-70ac5a6 # openapi-daemon
OK: all binaries report identical version v0.1.0-70ac5a6
```
**Edge cases tested:**
- **rc=0** for all three `--version` invocations; single-dash `-version` also works (stdlib accepts both).
- **Format regex** `^v0\.1\.0-[0-9a-f]{7}$` matches all three.
- **mcp stdin-hang regression guard:** running without `--version` prints `serving on stdio` and blocks (killed by timeout, rc=124); with `--version` it exits rc=0 instantly and the marker never appears — proving the early `return` precedes server start.
- **daemon usage-error path preserved:** `--bogus` still exits rc=2 with usage; the usage text now includes `-version`.
- **No-ldflags dev build** degrades gracefully to `v0.1.0-dev-none`.{"model": "deepseek-v4-flash", "problem_class": "go-cli-version-flag-surface-binaries", "result": "passed", "tests": 7}