4.11.3 Module Metadata & Version Semantics
recommendedFor a published ruleset, MODULE.bazel is not just setup boilerplate. It is the manifest downstream users consume through bazel_dep(): the module name they write, the version Bazel resolves, the direct dependencies that enter their module graph, and the Bazel-version constraints they may hit before loading any BUILD file.1 The migration mechanics of Bzlmod belong in M1 WORKSPACE → Bzlmod. Here the rule author's question is narrower: what metadata becomes part of the ruleset's public contract?
Make Module Identity Boring
The module name should be stable before broad publication. A bazel_dep(name = "...") names a module, and the apparent repository name defaults to that module name unless repo_name changes it.2 Renaming a ruleset module is therefore a consumer migration, not a cosmetic cleanup.
module(
name = "rules_foo",
bazel_compatibility = [">=8.0.0"],
)
bazel_dep(name = "dependency_module", version = "1.2.3")
Use repo_name as a migration bridge when the ruleset historically used a different self-repository name, not as a branding layer to keep changing.2 The public .bzl entry points from 4.11.1 Ruleset Layout & Public Entry Points should also use that stable naming story: if the docs say users load @rules_foo//foo:defs.bzl, the module metadata should not make that label feel like an accident.
Version metadata has a subtle source-of-truth rule. Current official guidance recommends avoiding a hard-coded version in the source archive's MODULE.bazel. Instead, the registry entry for a release should carry the released version, commonly by applying a generated patch during publish-to-bcr.3 That keeps tags, registry metadata, single_version_override, and non-registry overrides from disagreeing about what version a checkout represents.3 Older release recipes sometimes stamped a source placeholder to distinguish release archives from source overrides. If you inherit one, compare it against the current FAQ before copying it into a new ruleset.4
Treat bazel_dep() As A Published Constraint
Bazel module versions are a relaxed superset of SemVer, but bazel_dep() does not accept SemVer ranges or latest.5 Bazel uses Minimal Version Selection: after reading reachable module manifests, it selects the earliest version that satisfies all declared minimum requirements, which usually means the highest version requested anywhere in the graph, not the newest version that exists in a registry.6
That makes each ruleset dependency declaration a real constraint you publish to users. If rules_foo declares bazel_dep(name = "helper", version = "2.4.0"), every consuming graph that includes rules_foo may be lifted to at least helper@2.4.0. This is useful when the ruleset genuinely needs APIs from that version. It is harmful when the dependency is only needed to build, test, or release the ruleset itself.
For normal runtime dependencies, publish the lowest version the ruleset actually supports, not the newest version you happen to test today. The rule-author version of MVS is plain: non-root modules should require the lowest viable dependency versions so root modules can choose higher versions when their graph needs them.7 Test newer dependencies with root-only overrides or dedicated compatibility fixtures. Do not force every consumer graph forward just because your development checkout is current.7
Use dev_dependency = True for dependencies that should not propagate to downstream consumers. The API reference says such dependencies are ignored when the current module is not the root module, or when --ignore_dev_dependency is enabled.8 The practical reason is concrete: a ruleset may need language toolchains and libraries to build its own helper binaries from source, but release users should download prebuilt toolchains instead of inheriting those build-only dependencies through MVS.9 A BCR publication flow can patch those build-only dependencies to dev_dependency so released users see a smaller dependency surface.9
Related module families need an even more deliberate version story. Splitting Boost into many modules in BCR lets users depend on one library without fetching all of Boost. A boost.pin_version module plus mutual dependencies then keeps modules from one Boost release from mixing with another.10 The durable lesson is not the exact Boost layout. It is that a family of modules needs an alignment mechanism the module graph can see. If you publish rules_foo_core, rules_foo_python, and rules_foo_java, decide whether they can evolve independently or whether their bazel_dep() requirements should keep them in a tested band.
Be Careful With Compatibility Signals
bazel_compatibility is a check, not dependency resolution. It lets a module declare compatible Bazel versions, and Bzlmod uses that information to check the user's current Bazel version. It does not make Bazel choose a different module version.11 Use it for real minimum or excluded Bazel versions, then let 4.11.5 Ruleset CI/CD Patterns prove the claim with a version matrix.
compatibility_level is now a trap for new guidance. Legacy Bzlmod documentation treated it as the module-level breaking-change signal, but the current FAQ says to stop using it: starting with Bazel 8.6.0 and 9.1.0, both compatibility_level and max_compatibility_level are no-ops because increasing compatibility levels created hard-to-resolve conflicts for users.12 The current API reference also marks module(compatibility_level = ...) and bazel_dep(max_compatibility_level = ...) as deprecated no-ops.13
That does not mean breaking changes are invisible. It means the signal moves out of solver magic and into a human compatibility contract: clear error messages, documented migration paths, semantic versioning when the ruleset claims it, and CI across supported Bazel versions.12 The broader policy for attrs, providers, generated repositories, setup entry points, and Bazel-version support is 4.11.6 Rule Compatibility & Bazel Version Policy. For module metadata, keep the resolver unsurprising and make any compatibility signal match what Bazel actually enforces today.
Ruleset module metadata is public API. Keep the module name stable, keep the release version's source of truth in the registry workflow, declare only the dependencies downstream users should inherit, and use dev_dependency for build-only machinery.
Do not design new workflows around compatibility_level. In current Bazel lines it is a deprecated no-op. Breaking-change communication belongs in release notes, migration errors, SemVer policy, and CI-backed compatibility claims.
Check your understanding · 3 questions
1.For a published ruleset, where should the released module version usually be treated as the source of truth?
Select one answer
2.Which statements describe good ruleset MODULE.bazel metadata design?
Select all that apply
3.True or false: compatibility signals in MODULE.bazel.
Choose True or False for each sentence
Footnotes
-
Bazel modules - module manifests declare name, version, direct dependencies, and other metadata used during module resolution. ↩
-
MODULE.bazel files -
module()name, version,repo_name, andbazel_compatibilityparameters. ↩1 ↩2 -
Frequently asked questions - recommendation to avoid setting the release version in the source archive
MODULE.bazeland instead let registry metadata or publish automation provide it. ↩1 ↩2 -
Releasing Bazel rulesets that publish tools - release archive stamping and source-vs-release detection pattern used by an older Aspect ruleset release workflow. ↩
-
Bazel modules - relaxed SemVer module version format. Frequently asked questions - no SemVer ranges or
latestforbazel_dep(). ↩ -
Bazel modules - Minimal Version Selection chooses the earliest version satisfying declared requirements and keeps resolution reproducible. ↩
-
Migrating to Bazel Modules (a.k.a. Bzlmod) - Maintaining Compatibility, Part 1 - rulesets should declare minimum supported dependency versions with
bazel_dep(), test latest versions separately, and use root-only overrides or dev-only extension usage for development. ↩1 ↩2 -
MODULE.bazel files -
bazel_dep(dev_dependency = True)behavior. ↩ -
Releasing Bazel rulesets that publish tools - BCR publication patches Go build-tool dependencies to
dev_dependencyso release users do not inherit them. ↩1 ↩2 -
A manifest for Boost libraries in the Bazel Central Registry - per-library Boost modules,
boost.pin_version, mutual dependencies, and version-alignment rationale. ↩ -
MODULE.bazel files -
bazel_compatibilitychecks the current Bazel version but does not affect dependency resolution. ↩ -
Frequently asked questions -
compatibility_levelguidance, no-op status starting with Bazel 8.6.0 and 9.1.0, and replacement advice for clear migration errors. ↩1 ↩2 -
MODULE.bazel files -
module(compatibility_level = ...)andbazel_dep(max_compatibility_level = ...)marked deprecated no-ops. ↩