Hive Hive
Sign in

feat(cli): probe remote cache availability on bazel setup

GitHub issue · Closed

Metadata
Source
tuist/tuist #11466
Updated
Jun 25, 2026
Domains
CLI Kura
Details

What changed

tuist bazel setup now always probes the resolved remote cache endpoint before writing .bazelrc.tuist, regardless of how the URL was obtained, and fails closed when the probe does not succeed.

Why

Setup resolves the remote cache URL one of two ways:

  • From the server — it measures the latency of every advertised endpoint and discards unreachable ones, so setup already fails correctly when nothing answers.
  • From the TUIST_CACHE_ENDPOINT override — this short-circuited straight to the provided URL without any probe (and the single-endpoint server case did the same).

So with an override (or a single server endpoint) setup could write a .bazelrc.tuist pointing at a cache it had never actually contacted, deferring the failure to the first bazel build with a confusing error.

How

The probe issues the same REAPI handshake Bazel performs on start-up: build.bazel.remote.execution.v2.Capabilities/GetCapabilities over gRPC (plaintext for http endpoints, TLS otherwise), sending the bearer token, the x-tuist-account-handle header and instance_name = <project handle>.

This validates the actual path Bazel will use — connectivity, TLS termination, auth, and instance authorization — rather than the HTTP GET /up health check the latency selection relies on, which exercises a different protocol entirely. Kura already serves GetCapabilities (kura/src/reapi/mod.rs).

New TuistREAPI module

The REAPI client lives in a new TuistREAPI module, not TuistCAS. The Bazel Remote Execution API (build.bazel.remote.execution.v2) is a distinct protocol from the compilation-caching CAS (compilation_cache_service.cas.v1) that TuistCAS speaks, so the vendored proto, generated stubs, and the probe service belong together in their own module. TuistCAS is left untouched (it goes back to its original dependency set), and TuistBazelCommand now depends on TuistCAS (for CacheURLStore) + TuistREAPI (for the probe).

Single derivation site (GRPCEndpoint)

The cache-URL → gRPC mapping (scheme→plaintext/TLS, default 443/80 port) lives in one placeBazelSetupCommandService. It derives a GRPCEndpoint value (host, port, isTLS) once and feeds it to both the probe and the rendered .bazelrc.tuist, so the two can’t disagree about which endpoint Bazel will reach. GRPCEndpoint (in TuistREAPI) is a pure value type carrying only connection coordinates — no URL/cache knowledge — which keeps RemoteCacheProbeService a generic REAPI client that just connects and calls GetCapabilities.

Other design choices

  • Probe in BazelSetupCommandService, not in CacheURLStoregetCacheURL is shared with the cache runtime hot path; the availability gate is a setup-time concern, and placing it here covers the override, single-endpoint, and multi-endpoint paths uniformly without adding a probe to every cache operation.
  • GetCapabilities over the existing /up check/up is HTTP and does not prove the gRPC REAPI cache works for this account/instance. GetCapabilities is exactly what Bazel itself calls.
  • Minimal vendored protocli/Sources/TuistREAPI/capabilities.proto declares only the Capabilities service, GetCapabilitiesRequest{instance_name}, and an empty ServerCapabilities (proto3 ignores unknown response fields, so the full REAPI tree is unnecessary). Regenerate with mise run cli:generate-reapi-proto.
  • Fail-closed — a failed probe aborts setup with a descriptive error.

Notable details

  • RemoteCacheProbeService opens the CLI’s first outbound gRPC client (via GRPCNIOTransportHTTP2) with a 10s deadline. CAS save/load go over OpenAPI/REST today, and the only other gRPC use is a local unix-socket server in CacheStartCommandService.
  • The new module is registered in both build systems: Package.swift (for swift build) and Tuist/ProjectDescriptionHelpers/Module.swift (for the Tuist-generated Xcode project).
  • Generator/runtime version skew (heads-up for regeneration): the pinned protoc-gen-grpc-swift-2 emits a type: argument on MethodDescriptor that the resolved grpc-swift-2 runtime does not accept yet. It must be stripped from regenerated *.grpc.swift files until the runtime catches up. Documented in cli/Sources/TuistREAPI/AGENTS.md.

User impact

tuist bazel setup now fails fast with a clear message when the configured/selected cache is unreachable or misbehaving, instead of generating config that breaks at the first build. Two failure modes verified against a locally-built binary:

TUIST_CACHE_ENDPOINT Result .bazelrc.tuist
http://127.0.0.1:1 (dead) … not available (unavailable): Could not establish a connection not written
https://tuist.dev (reachable, not a gRPC cache) … not available (permissionDenied): Unexpected non-200 HTTP Status Code not written

Validation

  • TuistBazelCommandTests/BazelSetupCommandServiceTests9/9 pass, including the two probe tests, which assert the exact GRPCEndpoint derived for TLS-default (https, no port → :443/TLS), explicit-port (https://…:8443), and plaintext (http://…:5091) cache URLs.
  • The Tuist project generates cleanly with the new module (all Module enum switches handled).
  • swiftformat --lint and swiftlint clean on the changed sources.

🤖 Generated with Claude Code

Comments