quaint-telephone-89068
05/26/2026, 7:39 PMremote_cache::CommandRunner::update_action_cache silently drops output_directories from the ActionResult it writes via UpdateActionResult whenever the action's Command.output_directories contains "" (root-capture). The resulting AC entry has exit_code: 0, empty output_directories, and passes completeness-checking on the BB side because there are no referenced blobs left to validate. Downstream consumers receive a "successful" cache hit with no outputs, which silently merges in nothing and breaks the build several steps later with no usable error message.
Pants version
2.32.0
OS
Linux, but the issue is not OS dependent
Additional info
Reproduction
1. Define a Pants process that declares output_directories=[""] (REv2.0 root capture).
2. Configure [GLOBAL] remote_cache_write = true against any REv2 server that exposes a working ActionCache write path (we used Buildbarn bb-storage:20250827T121715Z).
3. Flush the AC so a fresh write happens.
4. Run the goal that exercises the process. Pants reports success; the worker ExecuteResponse includes a proper outputDirectories: [{ treeDigest: <2824-byte Tree of XVM/...> }].
5. Query the AC entry Pants just wrote:
# Synthetic example using REv2 Python proto bindings.
res = ac_stub.GetActionResult(GetActionResultRequest(
instance_name="fuse",
action_digest=Digest(hash="<the action digest from worker logs>", size_bytes=141),
))
print(res.output_directories) # -> [] (BUG: should contain the root tree)
print(res.execution_metadata.worker_completed_timestamp)
# -> seconds: 1, nanos: ~3e8 (also wrong, but cosmetic)
##Root Cause
make_tree_for_output_directory in src/rust/process_execution/remote/src/remote_cache.rs is:
pub(crate) fn make_tree_for_output_directory(
root_trie: &DigestTrie,
directory_path: RelativePath,
) -> Result<Option<(Tree, Vec<Digest>)>, String> {
let sub_trie = match root_trie.entry(&directory_path)? {
None => return Ok(None), // <-- triggered for empty path
Some(directory:Entry:Directory(d)) => d.tree(),
...
};
...
}
When directory_path is empty (e.g. from Command.output_directories = [""]), root_trie.entry(&path) iterates zero path components and returns Ok(None) (see DigestTrie::entry_helper in fs/src/directory.rs). The caller in make_action_result then hits its None => continue arm and silently drops the output:
for output_directory in &command.output_directories {
let (tree, file_digests) = match Self::make_tree_for_output_directory(...)? {
Some(res) => res,
None => continue, // <-- silently skips output_directories=[""]
};
...
}
The None => continue is intentional per #11772 (REAPI says missing declared outputs should be skipped, not errored). It just never accounted for the legitimate root-capture case where the "path" is the root itself.
pantsbuild/pants