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>
23 KiB
Design Document
Overview
Purpose: This feature gives every XCE component (controller, scheduler, worker, apiserver, xcecli, xceadmin) a build-time identity — the git commit it was built from and when it was built — visible from the running artifact itself, via a --version flag and (for the four long-running daemons) a startup log line.
Users: Operators and developers diagnosing a running deployment (the motivating incident: a stale scheduler binary kept running a known-fixed crash, and confirming that required manual ls -la mtime checks plus git merge-base --is-ancestor).
Impact: Extends the existing, partial C-side mechanism (controller/scheduler already have a commit-id-only --version via argp, but xce-build.sh never actually sets it, so it is empty on every local build today) to carry a build timestamp too, fills the worker/Go/Python gaps, and fixes the xce-build.sh gap that made the motivating incident possible in the first place.
Goals
- Every component's build artifact carries a git commit identifier (dirty-suffixed when applicable) and a build timestamp.
--versionon every component prints both and exits.- Controller, scheduler, worker, and apiserver additionally log both at INFO level on startup.
xce-build.sh(the dev-loop build path implicated in the motivating incident) actually sets these values — today it does not.- The values are byte-for-byte identical between
--versionoutput and the startup log line for the same artifact.
Non-Goals
- Automatically detecting or alerting that a running binary is stale relative to current source (out of scope per requirements' Boundary Context — this feature only makes the comparison possible).
- Version compatibility negotiation between components running different versions.
- Retrofitting version info into already-built/deployed binaries.
- Migrating worker's
getopt_long()argument parsing toargp(a much larger, unrelated change) — worker gets a hand-added--versioncase instead.
Boundary Commitments
This Spec Owns
- The two build-time values themselves (commit identifier, build timestamp) and how each of the six components' build steps receive them.
--versionoutput and (for the four daemons) the startup log line, for all six components.xce-build.sh's computation of both values for the dev-loop build path.- The nix packages' (
xce.nix,xcecli.nix) computation of both values for the production build path.
Out of Boundary
- Any runtime mechanism that compares a running binary's version against "current" source (no such comparison exists or is planned).
xceadmin's own semantic package version (pyproject.toml'sversion = "0.1.0") — untouched; build metadata is additive, displayed alongside it, not a replacement for it.- Worker's broader argument-parsing architecture.
Allowed Dependencies
- The existing
commit_idmeson option andVERSION_COMMIT_ID/MK_VERSIONC mechanism (src/meson.build,src/meson.options,src/include/utils.h) — extended, not replaced. - The existing Nix flake commit-id derivation (
self.shortRev/self.dirtyShortRev,src/flake.nix) — extended with a new build-time value, same wiring style. src/tools/go_build's existing env-var-drivengo buildinvocation pattern (already used for CGO flags).src/xceadmin/meson.build's existingcustom_target→meson-build.shargument-passing pattern.- Go's standard
-ldflags -Xpackage-variable injection; Cobra's built-inVersionfield (xcecli already uses Cobra); Click's built-inversion_option(xceadmin already uses Click).
Revalidation Triggers
- The
commit_idmeson option is renamed, removed, or its consumption insrc/include/utils.h'sMK_VERSION/STRmacros changes shape. xce-build.sh'sMESON_OPTSconstruction is refactored away from its current per-profile array pattern.src/tools/go_build's finalgo buildinvocation line changes.src/xceadmin/meson-build.sh's step ordering changes (the generated build-info module must land before thepip installstep that packages it).
Architecture
Existing Architecture Analysis
- Controller and scheduler already parse
--version/-Vfor free via GNUargp'sargp_program_versionmechanism (MK_VERSION("xce v0.0.1-", STR(VERSION_COMMIT_ID)), bothmain.cs) — this feature extends that same macro rather than adding a parallel one. VERSION_COMMIT_IDalready flows into every C component uniformly viaadd_project_arguments()at the top-levelsrc/meson.build— the new build-timestamp define follows the identical path, added once, inherited everywhere.- Worker's
getopt_long()-based parsing has no equivalent auto-version behavior and needs an explicit new case. gitis deliberately absent from the nix devShell's declared buildInputs (src/nixSupport/xce.nix) — the sandboxednix buildpath already avoids shelling out to git for exactly this reason (uses Nix's ownself.shortRevintrospection instead). The dev-loopxce-build.shpath runs insidenix develop, which layers onto the ambient host PATH rather than replacing it, so git IS usable there — and needs to be used there, sincexce-build.shtoday never setscommit_idat all (the actual gap behind the motivating incident).
Architecture Pattern & Boundary Map
Two independent computation sites (respecting the existing sandboxed-vs-ambient split) feed one resolved value pair into meson, which every language-specific consumption site reads from — never recomputes independently.
graph TB
DevScript[xce-build.sh git plus date]
NixPkg[xce.nix xcecli.nix self.shortRev plus preConfigure date]
MesonOpts[meson commit_id and build_time options]
CDefine[C VERSION_COMMIT_ID and VERSION_BUILD_TIME defines]
GoLdflags[go_build ldflags -X injection]
PyGen[meson-build.sh generated build info module]
Controller[controller and scheduler argp version]
Worker[worker getopt_long version case]
Apiserver[apiserver flag version]
Xcecli[xcecli cobra version]
Xceadmin[xceadmin click version_option]
DevScript --> MesonOpts
NixPkg --> MesonOpts
MesonOpts --> CDefine
MesonOpts --> GoLdflags
MesonOpts --> PyGen
CDefine --> Controller
CDefine --> Worker
GoLdflags --> Apiserver
GoLdflags --> Xcecli
PyGen --> Xceadmin
Architecture Integration:
- Selected pattern: single resolved-value-pair fan-out, matching the existing
commit_idmechanism's own shape. - Domain/feature boundaries: computation (2 sites, environment-specific) is fully separated from consumption (6 sites, one per component) — a consumption site never knows or cares which computation site produced its value.
- Existing patterns preserved:
argpversion macro (C), CobraVersionfield and Clickversion_option(already the idiomatic mechanism in each respective Go/Python tool),go_build's env-var plumbing,meson-build.sh's positional-arg plumbing. - New components rationale:
sim_prescan_session_count-style small, single-purpose additions only where a gap genuinely exists (worker's version case, Go's ldflags wiring, Python's generated module) — no new abstraction layer introduced. - Steering compliance: no new external dependency (
tech.md); wire/build-format changes are free pre-release (no back-compat burden); TDD required for every new behavior.
Technology Stack
| Layer | Choice / Version | Role in Feature | Notes |
|---|---|---|---|
| C build (controller/scheduler/worker) | Meson preprocessor defines (existing mechanism) | Carries commit_id (existing) + new build_time into every C translation unit |
src/meson.build:30 |
| C CLI (controller/scheduler) | GNU argp (existing) |
--version/-V auto-handling via argp_program_version |
Extend MK_VERSION macro |
| C CLI (worker) | getopt_long() (existing) |
New explicit --version case |
No parser migration |
| Go build | -ldflags -X (standard Go idiom, new to this repo) |
Injects main.GitCommit/main.BuildTime package vars |
Via go_build script + Makefile |
| Go CLI (apiserver) | flag package (existing) |
New -version flag |
Matches existing single-flag style |
| Go CLI (xcecli) | Cobra (existing) | rootCmd.Version field (built-in --version) |
Zero new parsing code |
| Python build | Meson custom_target (existing) |
New generated build-info module written before pip install |
meson-build.sh |
| Python CLI (xceadmin) | Click (existing) | click.version_option() (built-in --version) |
Zero new parsing code |
| Nix packaging | Nix flake (existing) | self.shortRev/self.dirtyShortRev (existing, eval-time) + a preConfigure build-phase date -u hook for build_time (task 2.2 round 2: builtins.currentTime was tried first but breaks pure-eval nix build, i.e. requires --impure, and yields non-UTC epoch seconds — rejected in review, corrected to a build-phase computation) |
xce.nix, xcecli.nix |
File Structure Plan
Modified Files
src/meson.options— add abuild_timestring option (default''), alongside the existingcommit_id.src/meson.build— add-DVERSION_BUILD_TIME=to the existingadd_project_arguments()call (line ~30).src/include/utils.h— extendMK_VERSION/introduce a companion macro so the version string includes the build time, not just the commit id.src/controller/main.c,src/scheduler/src/main.c— updateargp_program_versionconstruction to include build time; add the startup INFO log line (each already has one startup INFO call site to extend/follow).src/worker/src/main.c— add a--versionlong option + case (prints the same shape, exits 0); add the startup INFO log line (follows its own existing startup INFO call site).src/tools/xce-build.sh— computecommit_id(git,-dirtysuffixed) andbuild_time(UTCdate) and pass both via new-Dcommit_id=/-Dbuild_time=entries inMESON_OPTS, falling back to"unknown"ifgitfails (e.g. not a git checkout).src/nixSupport/xce.nix,src/nixSupport/xcecli.nix— apreConfigurebuild-phase hook runsdate -u +%Y-%m-%dT%H:%M:%SZand appends-Dbuild_time=tomesonFlagsArray(build-time, not eval-time, unlikecommit_id'sself.shortRevwiring insrc/flake.nix/src/nixSupport/default.nix, which is unchanged).src/tools/go_build— readXCE_GIT_COMMIT/XCE_BUILD_TIMEenv vars (empty-safe) and append-ldflags "-X main.GitCommit=... -X main.BuildTime=..."to the finalgo buildinvocation.src/frontend/apiserver/meson.build,src/frontend/xcecli/meson.build— pass the two new env vars into theircustom_target()calls (mirrors existing env var passing).src/frontend/Makefile— add the same-ldflagsconstruction (viagit/datedirectly, dev-loop path) tobuild-server/build-clitargets.src/frontend/apiserver/cmd/main.go— declarevar (GitCommit, BuildTime string); add a-versionflag; add/extend the startuplogger.Info(...)line.src/frontend/xcecli/cmd/main.go— declare the same two vars; setrootCmd.Version(Cobra); extend its own startuplogger.Info(...)line.src/xceadmin/meson.build— pass two new positional args (or env vars) tomeson-build.sh.src/xceadmin/meson-build.sh— write a small generated Python module (e.g._build_info.py) into the mirrored source tree, before thepip installstep, containing the two values (falls back to"unknown"if not provided).src/xceadmin/src/xceadmin/cli.py— read the generated module (import guarded for the case it's genuinely absent, e.g. a non-meson editable install) and wireclick.version_option()on the top-level group.
New Files
src/xceadmin/src/xceadmin/_build_info.py— generated at build time only (not checked in); a committed fallback stub (all fields"unknown") ships in-tree sopip install -e .(the documented fast-iteration path inbuild-deployment.md) still imports cleanly without having run the meson build.
No new C/Go/Python source files beyond the one generated-with-fallback-stub Python module above — every other change extends an existing file's existing responsibility (arg parsing gains a flag, a build script gains an option).
Requirements Traceability
| Requirement | Summary | Components | Interfaces | Flows |
|---|---|---|---|---|
| 1.1, 1.2 | Every artifact carries commit id + build time | All 6 consumption sites + both computation sites | MK_VERSION macro, Go ldflags vars, Python _build_info module |
Computation → meson → consumption |
| 1.3 | Dirty tree marked | xce-build.sh's git computation |
commit_id string carries -dirty suffix |
n/a |
| 1.4 | Unknown when undeterminable | xce-build.sh, meson-build.sh, go_build |
fallback to literal "unknown" |
n/a |
| 2.1 | --version prints and exits |
argp (controller/scheduler), worker's new case, apiserver's -version, Cobra Version, Click version_option |
six CLI entry points | n/a |
| 2.2 | --version and startup log agree |
shared macro/var/module per component | same source value read twice, never recomputed | n/a |
| 3.1, 3.2 | Startup log line at INFO | controller/scheduler/worker/apiserver startup sequences | existing first-INFO-line call sites, extended | Startup → log |
| 4.1, 4.2 | Build tooling confirms embedding | xce-build.sh, nix packages |
non-empty resolved value check before/after meson setup/build |
Build → verify |
| 4.3 | Accurate under every C build profile | src/meson.build's project-wide add_project_arguments() |
applies uniformly to asan/debug/release | n/a |
| 5.1, 5.2 | Values reflect actual tree/build event | xce-build.sh (git+date at invocation time), nix (self.shortRev at evaluation time + preConfigure date -u at build time) |
no hardcoded/stale values | n/a |
Components and Interfaces
C Daemons (controller, scheduler, worker)
Version macro
| Field | Detail |
|---|---|
| Intent | Carry commit id + build time into every C daemon's --version output and startup log |
| Requirements | 1.1, 1.2, 2.1, 2.2, 3.1, 3.2, 4.3 |
Responsibilities & Constraints
MK_VERSION(src/include/utils.h) extended to compose"xce v0.0.1-<commit_id> built <build_time>"(or equivalent) from the two new preprocessor defines — a single macro, both consumers (controller/scheduler'sargp_program_version, worker's hand-added case) call it, guaranteeing--versionand the startup log line render from the identical string.- Controller/scheduler: their existing single startup INFO line (
main.c:365/377+381respectively, per discovery) gets this version string appended or logged as an immediately-preceding line — never a second, independently-formatted string. - Worker: new
{"version", no_argument, NULL, OPT_VERSION}long option in its existinglong_opts[]/getopt_long()switch (main.c:358-364), printing the same macro's output and exiting 0 before any other startup work begins.
Contracts: Service [ ] / API [ ] / Event [ ] / Batch [ ] / State [ ]
Implementation Notes
- Integration: extends
src/include/utils.h's existingSTR/MK_VERSIONmacros — no new header. - Validation: a build with
commit_id/build_timeunset must still compile and print"unknown"fields, never a blank/garbled string. - Risks: worker's
getopt_long()short-option string ("n:l:vh") must gain a way to reach the new long-only option — use a long-option-only constant (no short equivalent), matching--peers's own existing precedent in the samelong_opts[]array.
Go Binaries (apiserver, xcecli)
ldflags version injection
| Field | Detail |
|---|---|
| Intent | Inject build-time values into each Go binary at link time, exposed via each CLI's native version mechanism |
| Requirements | 1.1, 1.2, 2.1, 2.2, 3.1, 3.2, 4.1, 4.2 |
Responsibilities & Constraints
src/tools/go_build(consumed by bothapiserver/meson.buildandxcecli/meson.build'scustom_target()) readsXCE_GIT_COMMIT/XCE_BUILD_TIMEfrom its environment (empty-safe — falls back to"unknown"when unset, e.g. a barego buildinvoked by hand) and appends-ldflags "-X main.GitCommit=$v1 -X main.BuildTime=$v2"to its existinggo buildinvocation.src/frontend/Makefile'sbuild-server/build-clitargets compute the same two values directly (git/date, dev-loop path, no meson involved) and pass the identical-ldflagsshape — same output format as the meson path, different computation site (mirrors the C-side split).apiserver/cmd/main.go: package-levelvar (GitCommit = "unknown"; BuildTime = "unknown"), a new-versionbool flag (checked first, beforeutils.InitLogger()— prints and exits, no logging side effects), and the existinglogger.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; version string passed intorouter.Execute(cm, version), which setsrootCmd.VersionbeforerootCmd.Execute()runs (Cobra then handles--versionnatively — 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 [ ]
Implementation Notes
- Integration:
go_build's env-var pattern already exists for CGO flags — this reuses it, not a new plumbing mechanism. - Validation:
--version/-versionoutput and the startup log's values must be the exact same two package vars, read twice, never two separate computations. - Risks: none identified —
-ldflags -Xonly accepts string vars, both fields are strings by construction.
Python (xceadmin)
Generated build-info module
| Field | Detail |
|---|---|
| Intent | Package build-time values into the wheel so xceadmin --version reflects the actual build, not just pyproject.toml's static semantic version |
| Requirements | 1.1, 1.2, 2.1, 4.1, 4.2 |
Responsibilities & Constraints
meson-build.shwrites_build_info.py(containingGIT_COMMIT/BUILD_TIMEstring constants, sourced from two new args/env vars,"unknown"fallback) into the mirrored source tree'sxceadmin/package directory, before thepip installstep (so it packages into both the dev venv install and the final wheel) — same ordering discipline the existing schema-sync step already follows (also runs beforepip install).cli.py's top-level@click.group()gains@click.version_option(version=f"{pkg_version} ({_build_info.GIT_COMMIT}, {_build_info.BUILD_TIME})"), importing_build_infodefensively (a committed fallback stub with"unknown"fields ships in-tree so a non-mesonpip install -e .— the documented fast-iteration path — still imports cleanly).- No startup log line (Requirement 3 scopes that to the four long-running daemons only; xceadmin is a one-shot CLI).
Contracts: Service [ ] / API [ ] / Event [ ] / Batch [ ] / State [ ]
Implementation Notes
- Integration: extends
meson-build.sh's existing custom_target sequence at exactly one new step, same file-write style already used for schema sync. - Validation:
pip install -e .(no meson) must still import cleanly against the committed fallback stub — this is the one file in this feature that must exist in two states (checked-in fallback vs. build-time-overwritten real values) and both must be valid Python. - Risks: the generated module must not be checked in with real values (would go stale immediately) — only the
"unknown"-only fallback stub is committed;.gitignore(or equivalent) must exclude the real generated file ifmeson-build.shever writes it into the actual source tree rather than only the mirrored build-tree copy (verify this during implementation — the mirroring step atmeson-build.shlines 32-46 should already keep it out of the real source tree, matching how the schema sync step works today).
Build Orchestration
xce-build.sh value computation
| Field | Detail |
|---|---|
| Intent | Close the actual gap behind the motivating incident — the dev-loop build path must set real values, not leave them empty |
| Requirements | 4.1, 4.2, 4.3, 5.1, 5.2 |
Responsibilities & Constraints
- Computes
commit_idviagit rev-parse --short HEAD, appending-dirtywhengit diff --quiet HEAD --(orgit status --porcelain) reports changes; falls back to"unknown"ifgititself fails (e.g. run against an extracted source tarball with no.git). - Computes
build_timeviadate -u +%Y-%m-%dT%H:%M:%SZ. - Appends
-Dcommit_id="$commit_id" -Dbuild_time="$build_time"toMESON_OPTS(xce-build.sh:70-72) for every profile, before the existingmeson setup/meson setup --reconfigurecalls.
Contracts: Service [ ] / API [ ] / Event [ ] / Batch [ ] / State [ ]
Implementation Notes
- Integration: a small addition right where
MESON_OPTSis already constructed — no restructuring of the profile dispatch. - Validation: the exact scenario from the motivating incident — build via
/xce-build scheduler debug, run--version, confirm it shows a real (non-empty, non-"unknown") commit id matchinggit rev-parse --short HEADat build time. - Risks: none — purely additive to an existing array.
Testing Strategy
Unit Tests
- C: a new scheduler/controller/worker test (or a shared
libtest ifMK_VERSION's logic is factored into a testable helper) asserting the version string contains both the commit id and build time defines when set, and"unknown"fallbacks when unset. - Go: a test in each
cmdpackage (or a shared helper) asserting-version/--versionoutput contains bothGitCommitandBuildTimevalues. - Python: a
pytestcase (xceadmin's existing suite, run bymeson-build.sh) assertingxceadmin --versionoutput contains the generated build-info fields, and that the committed fallback stub is itself valid, importable Python.
Integration Tests
xce-build.sh-driven build (any profile) followed by running--versionon the resulting controller/scheduler/worker binaries, asserting a real (non-"unknown") commit id is present — this is the regression test for the motivating incident itself.- A
go_build-driven meson build of apiserver/xcecli, followed by-version, asserting the same. xceadmin's existing meson-driven build+test+wheel cycle, followed by installing the wheel and runningxceadmin --version.
E2E / Consistency
- For at least one C daemon and one Go binary: start it, capture its startup log line, separately run
--version, and assert the two report byte-identical commit id and build time (Requirement 2.2).