Adds src/tools/test-build-version-regression.sh, a standalone script (same convention as test-commit-durability.sh/test-heartbeat-tolerance.sh) that runs real xce-build.sh scheduler builds in both asan and debug profiles, extracts the commit id from xce_scheduler --version, and fails loudly if it doesn't match a live git rev-parse --short HEAD (+ -dirty) -- the exact staleness this feature was built to make visible without forensic git commands. Tolerates xce-build.sh's own known-unrelated flaky test exit (bug-1560) via captured exit status rather than a blanket set -e bypass, so a genuine compile failure still fails the check. A built-in demonstration proves the comparison logic rejects a wrong hash, an empty commit id, and the literal "unknown" fallback. Verified independently by review with a full from- scratch re-run of both profiles. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
15 KiB
Implementation Plan
-
1. Foundation: meson build-time option and C project-wide define
- Add a
build_timemeson option (string, default'') alongside the existingcommit_idoption insrc/meson.options - Extend
src/meson.build's existingadd_project_arguments()call to also defineVERSION_BUILD_TIMEfor every C component, mirroring howVERSION_COMMIT_IDis already threaded through - Observable: a plain
meson setupwith neither option set still configures successfully (both defines resolve to empty string, no build failure) - Requirements: 1.1, 1.2, 1.4, 4.3
- Add a
-
2. Foundation: compute real values at both build entry points
-
2.1 (P) xce-build.sh computes and passes commit id and build time
- Compute
commit_idviagit rev-parse --short HEAD, appended with-dirtywhen the tree has uncommitted changes (git status --porcelainorgit diff --quiet HEAD --) - Compute
build_timeviadate -u +%Y-%m-%dT%H:%M:%SZ - Fall back to the literal string
unknownforcommit_idwhengititself fails (e.g. no.gitpresent) - Append
-Dcommit_id="$commit_id" -Dbuild_time="$build_time"toMESON_OPTSfor every build profile, before the existingmeson setup/meson setup --reconfigurecalls - After computing both values, verify each is non-empty (
build_timealways is;commit_idfalls back tounknownonly on a genuinegitfailure) and print a loud warning to stderr if either resolves empty anyway -- the build tooling must not silently proceed with a value it cannot confirm - Observable: running
/xce-build scheduler debugfollowed by the resulting binary's--versionshows a commit id matching the currentgit rev-parse --short HEADat build time, not empty; deliberately breaking the computation (e.g. temporarily renaming.git) prints the warning instead of silently succeeding - Requirements: 1.1, 1.2, 1.3, 1.4, 4.1, 4.2, 5.1, 5.2
- Boundary: xce-build.sh
- Compute
-
2.2 (P) Nix packages compute and pass build time
Add a— corrected during review (round 1 REJECTED):builtins.currentTime-derived build-time value insrc/flake.nixbuiltins.currentTimebreaks pure-evalnix build(requires--impure) and yields non-UTC epoch seconds. Implemented instead as apreConfigurebuild-phasedate -u +%Y-%m-%dT%H:%M:%SZhook inxce.nix/xcecli.nixappending tomesonFlagsArray, alongside the unchanged eval-time-Dcommit_id=wiring- Fail loudly (non-empty guard) if
date -uever produces empty output, before the value reachesmesonFlagsArray - Observable:
nix build .#xcecore_debugproduces a binary whose--versionshows a non-empty, ISO-8601 UTC build time, without anygitinvocation inside the sandboxed derivation, and without requiring--impure - Requirements: 1.1, 1.2, 1.4, 4.1, 4.2, 5.1, 5.2
- Boundary: Nix packaging
-
3. Core: C daemon version display
-
3.1 Extend the shared version macro with build time and unknown fallback
Extend—MK_VERSION/STRMK_VERSION/STRcannot express the empty-string-to-unknownsubstitution at the preprocessor level, so implemented as new runtime functionsxce_version_field()/xce_compose_version()(utils.h/utils.c) instead;MK_VERSION/STRmarked SUPERSEDED with an explicit doc comment and handoff notes for 3.2 (round 1 REJECTED for silently orphaningMK_VERSION+ undocumentedargp_program_versionfile-scope-const-initializer trap; round 2 APPROVED after adding the SUPERSEDED comment + NOTE FOR CALLERS guidance)- Both fields render as
unknownwhen their underlying define is empty (verified via"", not justNULL), never a blank/garbled string - Observable: a build with both meson options unset produces a
--versionstring containingunknownfor both fields, not empty gaps -- 5 new unit tests intest_utils.cconfirm this - Requirements: 1.4, 2.2
- Boundary: utils.h
-
3.2 (P) Controller and scheduler: version in --version and startup log
argp_program_versionmigrated toxce_compose_version()via astatic char version_buf[128]computed and assigned beforeargp_parse()in all 3 files (controller/main.c, scheduler/main.c, test.c); zero remainingMK_VERSIONcallers repo-wide outside its own definition- Startup log lines read the identical
version_buf, never recomputed -- extended to BOTH of scheduler's mutually-exclusive first-INFO branches (--peersand no---peers), not just the one literally quoted in this task's text, since Requirement 3.1's "when the daemon starts" is unconditional - Observable confirmed live (independently re-verified by review, not just self-reported):
xce_controller --version,xce_test --version, andxce_scheduler --versionall print commit id + build time and exit; all startup log branches for both daemons are byte-identical to their own--versionoutput - Requirements: 1.1, 1.2, 2.1, 2.2, 3.1, 3.2, 4.3
- Boundary: controller/main.c, scheduler/main.c, controller/testsuite/test.c
- Depends: 3.1
-
3.3 (P) Worker: add --version and startup log line
- Added
OPT_VERSION=257(long-only, beyond ASCII range, no collision with"n:l:vh"/OPT_PEERS=256); handling case computesxce_compose_version()into astatic char version_buf[128]and returns before any startup work (check_user()/logdir/xcecd_init/xcecd_start) - Both startup INFO log branches (
--peersand no---peers) extended to log the same buffer - Observable confirmed live (independently re-verified by review):
xcecd --versionprints and exits; both startup log branches (captured via a realsudodaemon run) are byte-identical to--versionoutput - Requirements: 1.1, 1.2, 2.1, 2.2, 3.1, 3.2, 4.3
- Boundary: worker/main.c
- Depends: 3.1
- Added
-
4. Core: Go binary version display
-
4.1 go_build script injects ldflags from environment
src/tools/go_buildreadsXCE_GIT_COMMIT/XCE_BUILD_TIMEvia[ -z ... ](correctly empty-safe, not just unset-safe) and appends-ldflags "-X main.GitCommit=... -X main.BuildTime=..."to itsgo buildinvocation, falling back tounknown- Confirmed (research finding, independently reproduced by review):
-Xon amainpackage 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 - Observable: invoking
go_buildwith neither env var set still produces a working binary whose injected vars readunknown - Requirements: 1.1, 1.2, 1.4, 4.1, 4.2
- Boundary: go_build script
-
4.2 apiserver and xcecli meson builds pass resolved values through
- Both meson.build files pass
XCE_GIT_COMMIT/XCE_BUILD_TIME(fromget_option('commit_id')/get_option('build_time')) intogo_build's env, UNQUOTED (go_build'sfix_input_envs()only quote-stripsCURRENT_SOURCE_DIR/BUILD_ROOT, not these two -- quoting them would embed literal"chars and corrupt the-ldflagsargument) - Verified live (independently, by review): real commit id + build time reach
go build's actual invocation and are embedded in both binaries' Go build-info metadata (go version -m) - Observable partially deferred by design: no
main.GitCommit/main.BuildTimesymbols exist in either binary yet (go tool nmconfirms) since apiserver/xcecli'smainpackages don't declare those vars until tasks 4.4/4.5 -- this task's own scope (_Depends: 4.1_only) is the env-var plumbing itself, not the full round trip; task 6.2 owns re-verifying that once 4.4 lands - Requirements: 1.1, 1.2, 4.1, 4.2
- Boundary: apiserver/meson.build, xcecli/meson.build
- Depends: 4.1
- Both meson.build files pass
-
4.3 (P) Makefile dev-loop path computes and passes the same values
GIT_COMMIT_HASH/GIT_STATUS/GIT_COMMIT/BUILD_TIME/VERSION_LDFLAGS(:=, computed once per invocation) mirrorxce-build.sh's dirty-check semantics andgo_build's exact-ldflagsshape;build-cli/build-serverrecipes reference$(VERSION_LDFLAGS)unknownfallback verified via empty-output detection (robust forgit rev-parse --short HEADspecifically: never succeeds with empty stdout)--versionCLI output not yet observable (tasks 4.4/4.5 not landed) -- verified instead viago version -m's embedded build-info metadata, matching livegit rev-parse --short HEAD+ dirty suffixmake build-server's pre-existing CGO failure (bug-1597) independently confirmed unrelated via a stash A/B (identical failure on the unmodified Makefile) -- not a regression- Requirements: 1.1, 1.2, 1.3, 1.4, 4.1, 4.2, 5.1, 5.2
- Boundary: Makefile
- Depends: 4.1
-
4.4 (P) apiserver: version flag, vars, and startup log
GitCommit/BuildTimepackage vars (defaultunknown);-versionflag checked beforeutils.PromptForTopDir/utils.InitLogger(), zero filesystem side effects (live-verified: no logs dir created)- Startup log extended via
zap.String("gitCommit", ...)/zap.String("buildTime", ...)structured fields on the existing"Initialize Logger done!"line, per design.md's explicit mandate - Observable confirmed live (independently re-verified by review, full round trip):
-versionand the startup log's first line are byte-identical (45c9b026-dirty, matchinggit rev-parse --short HEAD+ dirty suffix, and a real UTC timestamp) - Requirements: 1.1, 1.2, 2.1, 2.2, 3.1, 3.2
- Boundary: apiserver/cmd/main.go
- Depends: 4.1
-
4.5 (P) xcecli: version field and vars
GitCommit/BuildTimevars inmain.go;router.go'sExecute()extended with aversion stringparameter, settingrootCmd.VersionbeforerootCmd.Execute()runs (the sole call site, confirmed by both implementer and review)- Cobra's native
--version/-vauto-registers oncerootCmd.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 --versionprints commit id (matchinggit 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
- Boundary: xcecli/cmd/main.go, xcecli/router/router.go
- Depends: 4.1
-
5. Core: xceadmin version display
-
5.1 (P) meson-build.sh generates a build-info module before packaging
commit_id/build_timepassed as 2 new positional args ($6/$7, matching this custom_target's existing all-positional style); write block sits between schema-sync andpip install, with an independent-per-fieldunknownfallback and a file-existence guard checking the exact path it just wrote (the mirrored tree, not the real source tree)- Observable confirmed live (full real build+test+wheel cycle, independently re-run by review): mirrored tree's
_build_info.pyand the packaged wheel both contain real, matching, non-unknownvalues; 258 tests pass, 3 skips confirmed as documented live-HA e2e tests - Requirements: 1.1, 1.2, 1.4, 4.1, 4.2
- Boundary: xceadmin/meson.build, xceadmin/meson-build.sh
- Depends: 1
-
5.2 (P) Committed fallback stub and CLI version wiring
- Committed
src/xceadmin/src/xceadmin/_build_info.pywithGIT_COMMIT/BUILD_TIMEboth"unknown"-- exact variable names verified identical to 5.1'smeson-build.sh-generated module;cli.py's import is a genuinetry/except ImportError(inline fallback class), not a bare import click.version_option()wired directly on the top-level@click.group() def cli(); version string composed viaimportlib.metadata.version("xceadmin")plus both_build_infofields, no hand-parsedpyproject.toml- Observable confirmed live (independently re-verified by review, both install paths): fresh scratch-venv editable install (
pip install --no-deps -e ., no meson) ->xceadmin --versionprints0.1.0 (unknown, unknown), exit 0, no import error; the meson-built wheel installed into a separate scratch venv ->--versionshows real, non-unknowncommit id + build time. Full meson build/test: 261 passed, 3 skipped (258 baseline + 3 new tests) - Requirements: 1.1, 1.2, 2.1
- Boundary: xceadmin/src/xceadmin
- Depends: 5.1
- Committed
-
6. Integration: cross-toolchain consistency and regression verification
-
6.1 Motivating-incident regression test
- New standalone script
src/tools/test-build-version-regression.sh(follows the existingtest-commit-durability.sh/test-heartbeat-tolerance.shconvention: real-build regression check, not a mesontest()) runs realxce-build.sh schedulerbuilds in fresh dirs for bothasananddebug, extracts the commit id fromxce_scheduler --version, and compares it against a livegit rev-parse --short HEAD(+-dirty, using the identical dirty-detection logic asxce-build.shitself) - Deliberately tolerates only
xce-build.sh's own known-unrelated flaky-test exit code (bug-1560) via captured exit status, not a blanketset -ebypass -- a real compile failure still fails the check, since fresh build dirs mean no stale binary can survive to fool the binary-existence +--versiongate - Observable confirmed live (independently re-run twice, by both implementer and review, from-scratch builds each time): both asan and debug profiles PASS with a real, matching, non-
unknowncommit id; a built-in demonstration block proves the comparison logic correctly fails loudly on a wrong hash, an empty commit id, and the literalunknownfallback - Requirements: 4.1, 4.2, 4.3, 5.1, 5.2
- Depends: 2.1, 3.2, 3.3
- New standalone script
-
6.2 Version/startup-log consistency test
- For one C daemon (controller or scheduler) and one Go binary (apiserver), capture the startup log's version line and separately run
--version/-version, asserting the two report byte-identical commit id and build time - Observable: a passing test proves
--versionand the startup log can never disagree for the same artifact - Requirements: 2.2
- Depends: 3.2, 4.4
- For one C daemon (controller or scheduler) and one Go binary (apiserver), capture the startup log's version line and separately run
-
6.3 xceadmin end-to-end build/install/version cycle
- Run xceadmin's full meson-driven build (schema sync, venv install, pytest, wheel build), install the resulting wheel, and confirm
xceadmin --versionreports the generated (non-unknown) build-info fields - Observable: the installed wheel's
--versionoutput differs from an editable (pip install -e .) install'sunknown-only output, proving the generated module actually replaces the fallback - Requirements: 1.1, 1.2, 2.1, 4.1, 4.2
- Depends: 5.1, 5.2
- Run xceadmin's full meson-driven build (schema sync, venv install, pytest, wheel build), install the resulting wheel, and confirm
Implementation Notes
- Task 2.2:
builtins.currentTimecannot be used for anything feeding anix buildderivation without--impure— Nix's flake pure-eval mode strips it entirely, breaking the plain, documentednix build .#xcecore/.#xcecore_debugcommands. It also yields a raw epoch-seconds integer, not UTC ISO-8601. Any nix-side build metadata that must vary per-build (timestamps, etc.) belongs in a derivation'spreConfigure(or similar) build-phase shell hook using ordinary coreutils (date -u), appended tomesonFlagsArray— never in eval-time Nix expressions.self.shortRev/self.dirtyShortRevremain the correct, eval-time-safe mechanism specifically for the commit id (Nix computes those specially even under pure-eval), so this distinction is per-value, not a blanket "avoid nix expressions" rule.