feat(build-time-versioning): xcecli --version via Cobra rootCmd.Version

Adds GitCommit/BuildTime vars to xcecli's main.go. Cobra's rootCmd is
declared unexported in router.go (a different file than the task's
original main.go-only boundary), so main.go can't reach it directly --
resolved by extending router.go's sole exported entry point,
Execute(cm, version), to accept the composed version string and set
rootCmd.Version before rootCmd.Execute() runs. Cobra then auto-registers
--version/-v natively once rootCmd.Version is non-empty (confirmed via
reading Cobra v1.9.1 source directly) -- zero additional flag-parsing
code, as designed.

Live-verified via both build paths (meson xce-build.sh and make
build-cli): real commit id (matching git rev-parse --short HEAD plus
dirty suffix) and build time, exit 0.

xcecli's startup log deliberately not extended -- Requirement 3 scopes
that to the four daemons only; xcecli is a one-shot CLI. Corrects a
stale design.md line that had assumed otherwise.

Task 4.5/13 -- closes out the Go binary version display phase (tasks
4.1-4.5).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-08-20 14:07:38 +00:00
parent 17e837b196
commit fb09afe6be
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
4 changed files with 27 additions and 10 deletions

View file

@ -176,7 +176,7 @@ graph TB
- `src/tools/go_build` (consumed by both `apiserver/meson.build` and `xcecli/meson.build`'s `custom_target()`) reads `XCE_GIT_COMMIT`/`XCE_BUILD_TIME` from its environment (empty-safe — falls back to `"unknown"` when unset, e.g. a bare `go build` invoked by hand) and appends `-ldflags "-X main.GitCommit=$v1 -X main.BuildTime=$v2"` to its existing `go build` invocation. - `src/tools/go_build` (consumed by both `apiserver/meson.build` and `xcecli/meson.build`'s `custom_target()`) reads `XCE_GIT_COMMIT`/`XCE_BUILD_TIME` from its environment (empty-safe — falls back to `"unknown"` when unset, e.g. a bare `go build` invoked by hand) and appends `-ldflags "-X main.GitCommit=$v1 -X main.BuildTime=$v2"` to its existing `go build` invocation.
- `src/frontend/Makefile`'s `build-server`/`build-cli` targets compute the same two values directly (git/date, dev-loop path, no meson involved) and pass the identical `-ldflags` shape — same output format as the meson path, different computation site (mirrors the C-side split). - `src/frontend/Makefile`'s `build-server`/`build-cli` targets compute the same two values directly (git/date, dev-loop path, no meson involved) and pass the identical `-ldflags` shape — same output format as the meson path, different computation site (mirrors the C-side split).
- `apiserver/cmd/main.go`: package-level `var (GitCommit = "unknown"; BuildTime = "unknown")`, a new `-version` bool flag (checked first, before `utils.InitLogger()` — prints and exits, no logging side effects), and the existing `logger.Info("********** Initialize Logger done!")` call site extended to also log both values (same convention, `"********** <action> done!"` is untouched; the version fields ride as structured zap fields on that same line or an adjacent one). - `apiserver/cmd/main.go`: package-level `var (GitCommit = "unknown"; BuildTime = "unknown")`, a new `-version` bool flag (checked first, before `utils.InitLogger()` — prints and exits, no logging side effects), and the existing `logger.Info("********** Initialize Logger done!")` call site extended to also log both values (same convention, `"********** <action> done!"` is untouched; the version fields ride as structured zap fields on that same line or an adjacent one).
- `xcecli/cmd/main.go`: same two vars; `rootCmd.Version = fmt.Sprintf(...)` set once at init (Cobra then handles `--version` natively — zero new flag-parsing code); its own existing `logger.Info("********** Initialize Logger on client side done!")` extended the same way. - `xcecli/cmd/main.go`: same two vars; version string passed into `router.Execute(cm, version)`, which sets `rootCmd.Version` before `rootCmd.Execute()` runs (Cobra then handles `--version` natively — zero new flag-parsing code). Correction (task 4.5): xcecli's startup log is deliberately NOT extended — Requirement 3 scopes the startup-log obligation to "the controller, scheduler, worker, or apiserver daemon" only; xcecli is a one-shot CLI, not a daemon, and was never in Requirement 3's scope.
**Contracts**: Service [ ] / API [ ] / Event [ ] / Batch [ ] / State [ ] **Contracts**: Service [ ] / API [ ] / Event [ ] / Batch [ ] / State [ ]

View file

@ -6,7 +6,7 @@
- Observable: a plain `meson setup` with neither option set still configures successfully (both defines resolve to empty string, no build failure) - Observable: a plain `meson setup` with neither option set still configures successfully (both defines resolve to empty string, no build failure)
- _Requirements: 1.1, 1.2, 1.4, 4.3_ - _Requirements: 1.1, 1.2, 1.4, 4.3_
- [ ] 2. Foundation: compute real values at both build entry points - [x] 2. Foundation: compute real values at both build entry points
- [x] 2.1 (P) xce-build.sh computes and passes commit id and build time - [x] 2.1 (P) xce-build.sh computes and passes commit id and build time
- Compute `commit_id` via `git rev-parse --short HEAD`, appended with `-dirty` when the tree has uncommitted changes (`git status --porcelain` or `git diff --quiet HEAD --`) - Compute `commit_id` via `git rev-parse --short HEAD`, appended with `-dirty` when the tree has uncommitted changes (`git status --porcelain` or `git diff --quiet HEAD --`)
- Compute `build_time` via `date -u +%Y-%m-%dT%H:%M:%SZ` - Compute `build_time` via `date -u +%Y-%m-%dT%H:%M:%SZ`
@ -24,7 +24,7 @@
- _Requirements: 1.1, 1.2, 1.4, 4.1, 4.2, 5.1, 5.2_ - _Requirements: 1.1, 1.2, 1.4, 4.1, 4.2, 5.1, 5.2_
- _Boundary: Nix packaging_ - _Boundary: Nix packaging_
- [ ] 3. Core: C daemon version display - [x] 3. Core: C daemon version display
- [x] 3.1 Extend the shared version macro with build time and unknown fallback - [x] 3.1 Extend the shared version macro with build time and unknown fallback
- ~~Extend `MK_VERSION`/`STR`~~ — `MK_VERSION`/`STR` cannot express the empty-string-to-`unknown` substitution at the preprocessor level, so implemented as new runtime functions `xce_version_field()`/`xce_compose_version()` (`utils.h`/`utils.c`) instead; `MK_VERSION`/`STR` marked SUPERSEDED with an explicit doc comment and handoff notes for 3.2 (round 1 REJECTED for silently orphaning `MK_VERSION` + undocumented `argp_program_version` file-scope-const-initializer trap; round 2 APPROVED after adding the SUPERSEDED comment + NOTE FOR CALLERS guidance) - ~~Extend `MK_VERSION`/`STR`~~ — `MK_VERSION`/`STR` cannot express the empty-string-to-`unknown` substitution at the preprocessor level, so implemented as new runtime functions `xce_version_field()`/`xce_compose_version()` (`utils.h`/`utils.c`) instead; `MK_VERSION`/`STR` marked SUPERSEDED with an explicit doc comment and handoff notes for 3.2 (round 1 REJECTED for silently orphaning `MK_VERSION` + undocumented `argp_program_version` file-scope-const-initializer trap; round 2 APPROVED after adding the SUPERSEDED comment + NOTE FOR CALLERS guidance)
- Both fields render as `unknown` when their underlying define is empty (verified via `""`, not just `NULL`), never a blank/garbled string - Both fields render as `unknown` when their underlying define is empty (verified via `""`, not just `NULL`), never a blank/garbled string
@ -48,7 +48,7 @@
- _Boundary: worker/main.c_ - _Boundary: worker/main.c_
- _Depends: 3.1_ - _Depends: 3.1_
- [ ] 4. Core: Go binary version display - [x] 4. Core: Go binary version display
- [x] 4.1 go_build script injects ldflags from environment - [x] 4.1 go_build script injects ldflags from environment
- `src/tools/go_build` reads `XCE_GIT_COMMIT`/`XCE_BUILD_TIME` via `[ -z ... ]` (correctly empty-safe, not just unset-safe) and appends `-ldflags "-X main.GitCommit=... -X main.BuildTime=..."` to its `go build` invocation, falling back to `unknown` - `src/tools/go_build` reads `XCE_GIT_COMMIT`/`XCE_BUILD_TIME` via `[ -z ... ]` (correctly empty-safe, not just unset-safe) and appends `-ldflags "-X main.GitCommit=... -X main.BuildTime=..."` to its `go build` invocation, falling back to `unknown`
- Confirmed (research finding, independently reproduced by review): `-X` on a `main` package var that doesn't exist yet is silently ignored by the Go linker, not a build error -- this task has no real ordering dependency on 4.4/4.5 - Confirmed (research finding, independently reproduced by review): `-X` on a `main` package var that doesn't exist yet is silently ignored by the Go linker, not a build error -- this task has no real ordering dependency on 4.4/4.5
@ -81,11 +81,13 @@
- _Boundary: apiserver/cmd/main.go_ - _Boundary: apiserver/cmd/main.go_
- _Depends: 4.1_ - _Depends: 4.1_
- [ ] 4.5 (P) xcecli: version field and vars - [x] 4.5 (P) xcecli: version field and vars
- Declare the same two package vars in `src/frontend/xcecli/cmd/main.go`; set Cobra's built-in `rootCmd.Version` field from them once at init - `GitCommit`/`BuildTime` vars in `main.go`; `router.go`'s `Execute()` extended with a `version string` parameter, setting `rootCmd.Version` before `rootCmd.Execute()` runs (the sole call site, confirmed by both implementer and review)
- Observable: `xcecli --version` (Cobra's native handling) prints both values and exits - Cobra's native `--version`/`-v` auto-registers once `rootCmd.Version != ""` (confirmed via reading Cobra v1.9.1 source directly) -- zero additional flag-parsing code, as designed
- Observable confirmed live (independently re-verified by review, both build paths): `xcecli --version` prints commit id (matching `git rev-parse --short HEAD` + dirty suffix) and build time, exits 0
- xcecli's startup log deliberately NOT extended -- Requirement 3 scopes that obligation to the four daemons only; xcecli is a one-shot CLI (design.md corrected to match)
- _Requirements: 1.1, 1.2, 2.1_ - _Requirements: 1.1, 1.2, 2.1_
- _Boundary: xcecli/cmd/main.go_ - _Boundary: xcecli/cmd/main.go, xcecli/router/router.go_
- _Depends: 4.1_ - _Depends: 4.1_
- [ ] 5. Core: xceadmin version display - [ ] 5. Core: xceadmin version display

View file

@ -36,6 +36,19 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
) )
// GitCommit and BuildTime are injected at link time via
// `-ldflags "-X main.GitCommit=... -X main.BuildTime=..."` (see
// src/tools/go_build and src/frontend/Makefile's build-cli target). The
// "unknown" defaults matter: a bare `go build`/`go run` that bypasses that
// ldflags injection must still report "unknown" rather than an empty
// string, matching this feature's unknown-fallback requirement (see
// xce_version_field() in src/lib/utils.c for the equivalent C-side
// convention).
var (
GitCommit = "unknown"
BuildTime = "unknown"
)
func main() { func main() {
// init logger // init logger
@ -59,7 +72,8 @@ func main() {
} }
// execute the command // execute the command
if err := cmd.Execute(configManager); err != nil { version := fmt.Sprintf("%s built %s", GitCommit, BuildTime)
if err := cmd.Execute(configManager, version); err != nil {
fmt.Println(strings.ToLower(err.Error())) fmt.Println(strings.ToLower(err.Error()))
os.Exit(1) os.Exit(1)
} }

View file

@ -26,11 +26,12 @@ var (
) )
// execute the root command // execute the root command
func Execute(cm *config.ConfigManager) error { func Execute(cm *config.ConfigManager, version string) error {
if cm == nil { if cm == nil {
return fmt.Errorf("the config manager is not initialized") return fmt.Errorf("the config manager is not initialized")
} }
configManager = cm configManager = cm
rootCmd.Version = version
rootCmd.CompletionOptions.DisableDefaultCmd = true rootCmd.CompletionOptions.DisableDefaultCmd = true
rootCmd.SilenceErrors = true rootCmd.SilenceErrors = true
rootCmd.SilenceUsage = true rootCmd.SilenceUsage = true