Skip to content

Fix transformRuleInput prefix collision between fine-grained hub and spoke repos - #467

Merged
tinder-maxwellelliott merged 4 commits into
Tinder:masterfrom
hazel-sudzilouski-ai:hazel-sudz/fix-finegrained-prefix-collision
Aug 19, 2026
Merged

Fix transformRuleInput prefix collision between fine-grained hub and spoke repos#467
tinder-maxwellelliott merged 4 commits into
Tinder:masterfrom
hazel-sudzilouski-ai:hazel-sudz/fix-finegrained-prefix-collision

Conversation

@hazel-sudzilouski-ai

Copy link
Copy Markdown
Contributor

Summary

--fineGrainedHashExternalRepos matching in BazelRule.transformRuleInput used an unbounded string prefix (ruleInput.startsWith(it)). With hub-and-spoke external repos — e.g. rules_python's pip_parse in WORKSPACE mode, with a @pip hub of alias packages and one @pip_<pkg> spoke repo per package — every spoke label (@pip_numpy//...) prefix-matches the fine-grained hub name (@pip), so it is never rewritten to its //external:pip_numpy synthetic target. That seed is the only node whose hash flips when the spoke's pinned version changes (the version literal lives in the spoke repository rule's attrs), so consumers behind hub aliases were silently dropped from the impacted set on version bumps.

Related shape: the alias-wrap chain discussed in #197.

Fix: match repo names up to the // boundary (ruleInput == it || ruleInput.startsWith("$it//")).

  • First commit — the fix + a BazelRuleTest unit test pinning the rewrite behavior.
  • Second commit — a WORKSPACE-mode e2e fixture (hub_spoke_external: custom repository rule modeling the pip hub/spoke shape) + E2ETest.testHubSpokeVersionBumpImpactsConsumer_fineGrainedHubPrefixCollision, modeled on the issue-197 regression test.

Test plan

  • Red/green on the new e2e test: with the fix reverted to master it fails exactly on the consumer assertion (Got impacted: [//external:pip_numpy] — the orphaned spoke seed) while the seed-flip sanity assertion holds; with the fix it passes in ~9s.
  • bazel test //cli:BazelRuleTest //cli:RuleHasherTest — pass.
  • Full bazel test //cli:E2ETest — 39/39 pass (281s).
  • bazel run //cli/format — clean.
  • Validated at scale against a large internal monorepo replay of a real pip version bump: previously 0 of the directly-dependent tests were selected with the hub listed fine-grained; with this fix the full transitive consumer closure is selected, at no additional hashing cost (the spoke repos stay coarse — no wheel materialization).

🤖 Generated with Claude Code

hazel-sudzilouski-ai and others added 2 commits August 18, 2026 15:32
…spoke repos

fineGrainedHashExternalRepos matching used an unbounded string prefix, so
with hub-and-spoke repos (rules_python pip: @pip hub, @pip_<pkg> spokes) a
spoke label looked like it belonged to the fine-grained hub and was never
rewritten to its //external:<spoke> synthetic target -- the only node whose
hash flips when the spoke's pinned version changes. Consumers behind hub
aliases were therefore never impacted by version bumps.

Match on the full repo name up to the // boundary instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
New WORKSPACE-mode fixture (hub_spoke_external) models pip-style
hub-and-spoke external repos with a custom repository rule: a @pip hub of
alias packages in front of a @pip_numpy spoke whose repository-rule attrs
embed the pinned version. Bumping the version flips only the
//external:pip_numpy seed; the consumer behind the hub alias must follow
the chain @pip//numpy:pkg -> @pip_numpy//:lib -> //external:pip_numpy.

Fails without the transformRuleInput boundary fix (the spoke label
prefix-matched the fine-grained hub name and was never rewritten to its
seed); passes with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@hazel-sudzilouski-ai

Copy link
Copy Markdown
Contributor Author

@tinder-maxwellelliott we noticed this issue while using bazel-diff for rules_python in an internal repo.

transform_external_input matched fine-grained repos by unbounded string
prefix, the same bug the Kotlin transformRuleInput had: with hub-and-spoke
repos a spoke label (@pip_numpy//:lib) looked like it belonged to the
fine-grained hub (@pip) and was never rewritten to //external:pip_numpy,
the only node whose hash flips on a pinned version bump.

Match the repo name up to the // boundary, and cover it with a hash unit
test plus a Rust e2e test over the same hub_spoke_external fixture.

Co-authored-by: Cursor <cursoragent@cursor.com>

@tinder-maxwellelliott tinder-maxwellelliott left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the fix!

@hazel-sudzilouski-ai

Copy link
Copy Markdown
Contributor Author

thanks for the review! would you be so kind as to cut a new release after this if possible so we can test?

@tinder-maxwellelliott

Copy link
Copy Markdown
Collaborator

thanks for the review! would you be so kind as to cut a new release after this if possible so we can test?

Will do

The hub_spoke_external fixture declares @pip and @pip_numpy in a WORKSPACE
file, and Bazel 9 removed WORKSPACE support outright -- --enable_workspace
is inert there, so the fixture's repos never exist and generate-hashes
fails with "No repository visible as '@pip'". That failed the jre21 9.x
jobs while 8.x passed.

Gate the Kotlin and Rust e2e cases on a Bazel that still loads WORKSPACE,
matching the version gating the bzlmod e2e tests already use. The matching
logic itself stays pinned on every version by BazelRuleTest and the Rust
hash unit tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
@tinder-maxwellelliott
tinder-maxwellelliott merged commit 626ee3b into Tinder:master Aug 19, 2026
25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants