Summary
With bazel-diff v49.1.0, changing a file in an explicitly selected fine-grained external repository does not mark its workspace consumer as impacted when the dependency chain includes an intermediate module that is not directly visible from the root.
Both hash-generation commands succeed with --no-keep_going. The leaf's hash changes, but the consumer's hash does not.
Explicitly including every repository in the wrapper chain restores propagation. I would like to clarify whether that enumeration is required by the intended contract, or whether this is a remaining coverage gap related to #197.
Environment
- bazel-diff v49.1.0, published native macOS ARM64 binary
- Bazel 8.7.0
- Bzlmod with
local_path_override
- Unconfigured query mode
- No query exclusions or custom content hashes
I reproduced this locally. The script below was run with an environment-specific addition routing downloads through an internal artifact mirror. The fixture and reproduction commands were unchanged. Module discovery completed successfully, and both snapshots contained identical module-graph metadata.
Dependency structure
//:consumer
-> @facade//:export
-> @@middle+//:wrapped
-> @leaf//:value
-> @leaf//:value.txt
The root declares facade and leaf. Only facade declares middle, under the apparent name private_wrapper. Therefore, @private_wrapper is valid inside facade, but not from the root.
An independent bazel query 'deps(//:consumer)' confirms the chain above.
Minimal reproduction
Assumes the v49.1.0 native executable is available as bazel-diff, and bazel runs Bazel 8.7.0.
set -euo pipefail
work="$(mktemp -d)"
mkdir -p "$work"/{root,facade,middle,leaf,results}
cat > "$work/root/.bazelversion" <<'EOF'
8.7.0
EOF
cat > "$work/root/MODULE.bazel" <<'EOF'
module(name = "identity_probe")
bazel_dep(name = "facade", version = "1.0")
bazel_dep(name = "leaf", version = "1.0")
local_path_override(module_name = "facade", path = "../facade")
local_path_override(module_name = "middle", path = "../middle")
local_path_override(module_name = "leaf", path = "../leaf")
EOF
cat > "$work/root/BUILD.bazel" <<'EOF'
filegroup(name = "consumer", srcs = ["@facade//:export"])
filegroup(name = "control", srcs = ["control.txt"])
EOF
printf 'unchanged\n' > "$work/root/control.txt"
cat > "$work/facade/MODULE.bazel" <<'EOF'
module(name = "facade", version = "1.0")
bazel_dep(name = "middle", version = "1.0", repo_name = "private_wrapper")
EOF
cat > "$work/facade/BUILD.bazel" <<'EOF'
alias(
name = "export",
actual = "@private_wrapper//:wrapped",
visibility = ["//visibility:public"],
)
EOF
cat > "$work/middle/MODULE.bazel" <<'EOF'
module(name = "middle", version = "1.0")
bazel_dep(name = "leaf", version = "1.0")
EOF
cat > "$work/middle/BUILD.bazel" <<'EOF'
alias(
name = "wrapped",
actual = "@leaf//:value",
visibility = ["//visibility:public"],
)
EOF
cat > "$work/leaf/MODULE.bazel" <<'EOF'
module(name = "leaf", version = "1.0")
EOF
cat > "$work/leaf/BUILD.bazel" <<'EOF'
filegroup(
name = "value",
srcs = ["value.txt"],
visibility = ["//visibility:public"],
)
EOF
printf 'before\n' > "$work/leaf/value.txt"
cd "$work/root"
bazel-diff generate-hashes \
-w "$PWD" -b bazel \
--fineGrainedHashExternalRepos=@leaf \
--no-keep_going --verbose \
"$work/results/before.json"
printf 'after\n' > "$work/leaf/value.txt"
bazel-diff generate-hashes \
-w "$PWD" -b bazel \
--fineGrainedHashExternalRepos=@leaf \
--no-keep_going --verbose \
"$work/results/after.json"
bazel-diff get-impacted-targets \
-w "$PWD" -b bazel \
--excludeExternalTargets=true \
-sh "$work/results/before.json" \
-fh "$work/results/after.json" \
-o "$work/results/impacted.txt"
cat "$work/results/impacted.txt"
The output files are outside the queried workspace to avoid contaminating subsequent snapshots.
Actual result
Both generate-hashes commands exit successfully. Verbose output includes:
[Warn] Unable to calculate digest for input @facade//:export for rule //:consumer
The impacted targets are:
@leaf//:value
@leaf//:value.txt
//:consumer is missing.
Expected result
I expected //:consumer to be impacted because its transitive input changed, consistent with the wrapped-external-repository behavior covered by #197.
Controls and working configuration
Repeating the comparison with this repository list restores propagation:
--fineGrainedHashExternalRepos=@leaf,@@leaf+,@facade,@@facade+,@@middle+
The canonical names above were obtained from Bazel for this fixture.
| Observation |
Leaf-only selection |
Explicit full chain |
| No-op snapshots |
Identical hashes; no impacts |
Identical hashes; no impacts |
| Leaf content change |
Leaf impacted; consumer missed |
Leaf and consumer impacted |
| Unrelated control during leaf change |
Unchanged |
Unchanged |
Change only control.txt |
Only control and its source impacted |
Only control and its source impacted |
| Stored module-graph metadata during leaf change |
Unchanged |
Unchanged |
With the full chain selected, the recorded rule dependency edges connect the consumer through both aliases to the leaf. The consumer is not relying on a coarse synthetic repository edge for propagation.
The existing wrapped_external_repo regression fixture declares both its inner and middle modules directly in the root. This reproduction adds a wrapper that is only visible through another module, which appears to expose a case not covered by that fixture.
Possible fix direction
It looks like bazel-diff may need to include the intermediate wrapper targets when following changes from a fine-grained repository to its consumers. In this example, explicitly including the full chain makes the change reach //:consumer.
One possible approach would be to ask Bazel for that dependency chain and use the repository names Bazel returns, rather than expanding module names. That might also avoid pulling unrelated development packages into the query.
I am not sure whether this fits the intended design. If callers are expected to list every intermediate wrapper explicitly, that seems cumbersome and potentially brittle. The list could become stale when transitive dependencies change, leading to missed impacts without an obvious failure. It would be preferable if bazel-diff could discover those connections itself.
The example above could serve as a regression case: the leaf change should reach the consumer, while no-op runs and unrelated targets remain unchanged.
Summary
With bazel-diff v49.1.0, changing a file in an explicitly selected fine-grained external repository does not mark its workspace consumer as impacted when the dependency chain includes an intermediate module that is not directly visible from the root.
Both hash-generation commands succeed with
--no-keep_going. The leaf's hash changes, but the consumer's hash does not.Explicitly including every repository in the wrapper chain restores propagation. I would like to clarify whether that enumeration is required by the intended contract, or whether this is a remaining coverage gap related to #197.
Environment
local_path_overrideI reproduced this locally. The script below was run with an environment-specific addition routing downloads through an internal artifact mirror. The fixture and reproduction commands were unchanged. Module discovery completed successfully, and both snapshots contained identical module-graph metadata.
Dependency structure
The root declares
facadeandleaf. Onlyfacadedeclaresmiddle, under the apparent nameprivate_wrapper. Therefore,@private_wrapperis valid insidefacade, but not from the root.An independent
bazel query 'deps(//:consumer)'confirms the chain above.Minimal reproduction
Assumes the v49.1.0 native executable is available as
bazel-diff, andbazelruns Bazel 8.7.0.The output files are outside the queried workspace to avoid contaminating subsequent snapshots.
Actual result
Both
generate-hashescommands exit successfully. Verbose output includes:The impacted targets are:
//:consumeris missing.Expected result
I expected
//:consumerto be impacted because its transitive input changed, consistent with the wrapped-external-repository behavior covered by #197.Controls and working configuration
Repeating the comparison with this repository list restores propagation:
The canonical names above were obtained from Bazel for this fixture.
control.txtWith the full chain selected, the recorded rule dependency edges connect the consumer through both aliases to the leaf. The consumer is not relying on a coarse synthetic repository edge for propagation.
The existing
wrapped_external_reporegression fixture declares both its inner and middle modules directly in the root. This reproduction adds a wrapper that is only visible through another module, which appears to expose a case not covered by that fixture.Possible fix direction
It looks like bazel-diff may need to include the intermediate wrapper targets when following changes from a fine-grained repository to its consumers. In this example, explicitly including the full chain makes the change reach
//:consumer.One possible approach would be to ask Bazel for that dependency chain and use the repository names Bazel returns, rather than expanding module names. That might also avoid pulling unrelated development packages into the query.
I am not sure whether this fits the intended design. If callers are expected to list every intermediate wrapper explicitly, that seems cumbersome and potentially brittle. The list could become stale when transitive dependencies change, leading to missed impacts without an obvious failure. It would be preferable if bazel-diff could discover those connections itself.
The example above could serve as a regression case: the leaf change should reach the consumer, while no-op runs and unrelated targets remain unchanged.