myxce/.kiro/specs/build-time-versioning/tasks.md
Rongsong Shen 084a5a95c3
test(build-time-versioning): add xceadmin end-to-end build/install/version cycle check
Adds src/tools/test-xceadmin-build-install-version.sh (same house style as
tasks 6.1/6.2), which runs xceadmin's full real meson-driven build cycle
(schema sync, venv install, pytest, wheel build), installs the resulting
wheel into a fresh scratch venv, and separately does an editable install
into a second fresh scratch venv. Asserts the wheel install carries real,
non-"unknown" commit id and build time while the editable install carries
the committed "unknown"/"unknown" fallback stub, and explicitly asserts
the two differ -- proving the generated build-info module genuinely
replaces the fallback rather than both installs coincidentally agreeing.

Verified independently by review with a full from-scratch re-run: 261
passed/3 skipped (matching tasks 5.1/5.2's established baseline), both
install paths correct, and the demonstration block's crux case (identical
build-info values must be rejected by the differ-check) confirmed correct.

This is the final task of the build-time-versioning spec; all 13 sub-tasks
are now complete.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 17:02:01 +00:00

17 KiB

Implementation Plan

  • 1. Foundation: meson build-time option and C project-wide define

    • Add a build_time meson option (string, default '') alongside the existing commit_id option in src/meson.options
    • Extend src/meson.build's existing add_project_arguments() call to also define VERSION_BUILD_TIME for every C component, mirroring how VERSION_COMMIT_ID is already threaded through
    • 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
  • 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_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
    • Fall back to the literal string unknown for commit_id when git itself fails (e.g. no .git present)
    • Append -Dcommit_id="$commit_id" -Dbuild_time="$build_time" to MESON_OPTS for every build profile, before the existing meson setup/meson setup --reconfigure calls
    • After computing both values, verify each is non-empty (build_time always is; commit_id falls back to unknown only on a genuine git failure) 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 debug followed by the resulting binary's --version shows a commit id matching the current git rev-parse --short HEAD at 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
  • 2.2 (P) Nix packages compute and pass build time

    • Add a builtins.currentTime-derived build-time value in src/flake.nix — corrected during review (round 1 REJECTED): builtins.currentTime breaks pure-eval nix build (requires --impure) and yields non-UTC epoch seconds. Implemented instead as a preConfigure build-phase date -u +%Y-%m-%dT%H:%M:%SZ hook in xce.nix/xcecli.nix appending to mesonFlagsArray, alongside the unchanged eval-time -Dcommit_id= wiring
    • Fail loudly (non-empty guard) if date -u ever produces empty output, before the value reaches mesonFlagsArray
    • Observable: nix build .#xcecore_debug produces a binary whose --version shows a non-empty, ISO-8601 UTC build time, without any git invocation 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/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
    • Observable: a build with both meson options unset produces a --version string containing unknown for both fields, not empty gaps -- 5 new unit tests in test_utils.c confirm this
    • Requirements: 1.4, 2.2
    • Boundary: utils.h
  • 3.2 (P) Controller and scheduler: version in --version and startup log

    • argp_program_version migrated to xce_compose_version() via a static char version_buf[128] computed and assigned before argp_parse() in all 3 files (controller/main.c, scheduler/main.c, test.c); zero remaining MK_VERSION callers 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 (--peers and 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, and xce_scheduler --version all print commit id + build time and exit; all startup log branches for both daemons are byte-identical to their own --version output
    • 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 computes xce_compose_version() into a static char version_buf[128] and returns before any startup work (check_user()/logdir/xcecd_init/xcecd_start)
    • Both startup INFO log branches (--peers and no---peers) extended to log the same buffer
    • Observable confirmed live (independently re-verified by review): xcecd --version prints and exits; both startup log branches (captured via a real sudo daemon run) are byte-identical to --version output
    • Requirements: 1.1, 1.2, 2.1, 2.2, 3.1, 3.2, 4.3
    • Boundary: worker/main.c
    • Depends: 3.1
  • 4. Core: Go binary version display

  • 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
    • 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
    • Observable: invoking go_build with neither env var set still produces a working binary whose injected vars read unknown
    • 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 (from get_option('commit_id')/get_option('build_time')) into go_build's env, UNQUOTED (go_build's fix_input_envs() only quote-strips CURRENT_SOURCE_DIR/BUILD_ROOT, not these two -- quoting them would embed literal " chars and corrupt the -ldflags argument)
    • 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.BuildTime symbols exist in either binary yet (go tool nm confirms) since apiserver/xcecli's main packages 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
  • 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) mirror xce-build.sh's dirty-check semantics and go_build's exact -ldflags shape; build-cli/build-server recipes reference $(VERSION_LDFLAGS)
    • unknown fallback verified via empty-output detection (robust for git rev-parse --short HEAD specifically: never succeeds with empty stdout)
    • --version CLI output not yet observable (tasks 4.4/4.5 not landed) -- verified instead via go version -m's embedded build-info metadata, matching live git rev-parse --short HEAD + dirty suffix
    • make 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/BuildTime package vars (default unknown); -version flag checked before utils.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): -version and the startup log's first line are byte-identical (45c9b026-dirty, matching git 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/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)
    • 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
    • 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_time passed as 2 new positional args ($6/$7, matching this custom_target's existing all-positional style); write block sits between schema-sync and pip install, with an independent-per-field unknown fallback 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.py and the packaged wheel both contain real, matching, non-unknown values; 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.py with GIT_COMMIT/BUILD_TIME both "unknown" -- exact variable names verified identical to 5.1's meson-build.sh-generated module; cli.py's import is a genuine try/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 via importlib.metadata.version("xceadmin") plus both _build_info fields, no hand-parsed pyproject.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 --version prints 0.1.0 (unknown, unknown), exit 0, no import error; the meson-built wheel installed into a separate scratch venv -> --version shows real, non-unknown commit 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
  • 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 existing test-commit-durability.sh/test-heartbeat-tolerance.sh convention: real-build regression check, not a meson test()) runs real xce-build.sh scheduler builds in fresh dirs for both asan and debug, extracts the commit id from xce_scheduler --version, and compares it against a live git rev-parse --short HEAD (+-dirty, using the identical dirty-detection logic as xce-build.sh itself)
    • Deliberately tolerates only xce-build.sh's own known-unrelated flaky-test exit code (bug-1560) via captured exit status, not a blanket set -e bypass -- a real compile failure still fails the check, since fresh build dirs mean no stale binary can survive to fool the binary-existence + --version gate
    • 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-unknown commit id; a built-in demonstration block proves the comparison logic correctly fails loudly on a wrong hash, an empty commit id, and the literal unknown fallback
    • Requirements: 4.1, 4.2, 4.3, 5.1, 5.2
    • Depends: 2.1, 3.2, 3.3
  • 6.2 Version/startup-log consistency test

    • New standalone script src/tools/test-version-startup-consistency.sh (same house style as task 6.1's test-build-version-regression.sh) builds xce_scheduler and xce_apiserver for real, starts each daemon, greps its startup log, and asserts the commit-id/build-time VALUES (not whole-line text, since the C daemon's plain-text log line and apiserver's structured zap-JSON fields are differently shaped by design) are identical between --version/-version output and the startup log
    • apiserver's build routes around two pre-existing, unrelated, independently-reproduced sandbox blockers -- make build-server's missing CGO env (bug-1597) and xce-build.sh apiserver's go_build-hardcoded GOPROXY=goproxy.cn hanging in this sandbox (confirmed live by review via orphaned processes) -- via a direct go build -mod=readonly -ldflags invocation computing commit_id/build_time with the identical logic and injecting them via the identical -X shape go_build itself uses; only the orchestration layer differs, not what's injected or how task 4.4's unmodified main.go consumes it
    • Observable confirmed live (independently re-run in full by both implementer and review, real builds + real daemon startups each time): scheduler and apiserver both PASS with matching, real commit id + build time; a demonstration block proves the check correctly rejects a commit-id mismatch, a build-time mismatch, and an empty field on either side
    • Requirements: 2.2
    • Depends: 3.2, 4.4
  • 6.3 xceadmin end-to-end build/install/version cycle

    • New standalone script src/tools/test-xceadmin-build-install-version.sh (same house style as tasks 6.1/6.2) runs the full real xce-build.sh xceadmin cycle (schema sync, venv install, pytest, wheel build), installs the resulting wheel into a fresh scratch venv, and separately does an editable install (pip install -e src/xceadmin) into a second fresh scratch venv
    • Explicit differ-assertion (not just two independent pass/fail checks) that the wheel install's and editable install's build-info fields are NOT equal -- proving the generated module genuinely replaces the fallback rather than both installs coincidentally agreeing
    • Observable confirmed live (independently re-run in full by both implementer and review, real build+two installs each time): wheel install shows real, non-unknown commit id + build time; editable install shows the committed unknown/unknown fallback; a demonstration block proves the check rejects a wheel install showing unknown, an editable install showing a real-looking hash, and -- the crux case -- two identical build-info outputs fed to the differ-check
    • Requirements: 1.1, 1.2, 2.1, 4.1, 4.2
    • Depends: 5.1, 5.2

Implementation Notes

  • Task 2.2: builtins.currentTime cannot be used for anything feeding a nix build derivation without --impure — Nix's flake pure-eval mode strips it entirely, breaking the plain, documented nix build .#xcecore/.#xcecore_debug commands. 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's preConfigure (or similar) build-phase shell hook using ordinary coreutils (date -u), appended to mesonFlagsArray — never in eval-time Nix expressions. self.shortRev/self.dirtyShortRev remain 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.