What
Local Xcode builds reported via tuist inspect build now show the Module Cache tab on their build run page, the same breakdown previously exclusive to tuist test / tuist xcodebuild build.
Rather than re-uploading the dependency graph on every build, a build is linked to the graph the last tuist generate already uploaded, via a new generation_id.
Why
The Module Cache tab is backed by xcode_targets rows, which are created only when a command event arrives carrying an xcode_graph (analytics_controller → Xcode.create_xcode_graph). The build run page finds them through a command event whose build_run_id == build.id.
tuist inspect build emits no command event (shouldTrackAnalytics = false in TuistCommand.swift), so the graph never reaches the server for a local build and the tab stays hidden.
tuist generate already uploads the full graph + binary-cache hit breakdown as its own command event. The missing piece was a precise link from a local build back to that generation.
How
A CLI-minted generation_id (UUID) ties them together:
tuist generate mints it, records it on the run so the generate command event (which already carries the graph) is stamped with it, and persists it per-project in the Tuist cache (GenerationMetadataStore, keyed by the generated workspace path, pruned after 30 days).
UploadBuildRunService reads it back (by project path) and sends it on the createBuild upload, landing on build_runs.generation_id. Both tuist inspect build and tuist xcodebuild build benefit; a missing entry degrades gracefully.
- The build page resolves the generate command event via
CommandEvents.get_command_event_by_generation_id/2 and reuses the existing has_binary_cache_data? / binary_cache_analytics / binary_cache_counts unchanged.
Why a CLI-minted id (not the server’s command-event id)
tuist generate uploads its command event through the background analytics-upload subprocess, so the main process never learns the server-assigned id. A CLI UUID sidesteps that and makes the link precise across developers, machines, and branches (a temporal “most recent generation” heuristic would mismatch on shared projects).
The new generation_id columns are nullable UUID on command_events (with an idx_generation_id bloom filter) and build_runs, mirroring the existing build_run_id plumbing exactly. The build’s own command_event is kept separate from the binary_cache_command_event so the page’s metadata row still shows the real build command, not “tuist generate”.
Impact
Customers who wire tuist inspect build into their Xcode builds get the Module Cache breakdown for plain Xcode builds, not just tuist test. No server schema change to xcode_targets; no extra command event per build (the graph is uploaded once per generation, not per build).
Accepted limitation
The breakdown reflects the project state at the last tuist generate, not manual Xcode edits made afterward.
Validation
- Server (
mix test): command_events + build_run_live (14/0, including an end-to-end test of build → generation_id → generate command event → Module Cache tab), plus builds + builds_controller (86/0). Migrations apply via the test alias. mix format + Credo clean (no new issues).
- CLI: full workspace
build-for-testing compiles with 0 errors (including the new GenerationMetadataStoreTests and the GenerateService / UploadBuildRunService / CommandEventFactory test changes); swiftformat + swiftlint clean (no new violations). Test execution is blocked only by the sandbox’s lack of a PTY, so the suites run in CI.
🤖 Generated with Claude Code