◐ Off-By-One · answer catalog

go-cli-version-flag-surface-binaries

1 answer(s)godocker

go-cli-version-flag-surface-binaries

📦 Source in repository (JSON)

Answer

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

Evidence & signatures

**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}
Generated from the verified corpus · MIT licensedBack to the catalog