4.11.6 Rule Compatibility & Bazel Version Policy

recommended

Publishing a ruleset turns implementation choices into promises. Users need to know which Bazel versions you test, which Starlark symbols and generated repositories are stable, and how much warning they get before a breaking change. The goal is not perfect compatibility forever. The goal is a migration path where users can change one major thing at a time: first the ruleset, or first Bazel, but not both in the same emergency.1

Two Version Lines Move At Once

A ruleset has its own release line, and Bazel has its release line. A compatibility policy explains how those two lines overlap. A manageable migration is one where users are not forced to upgrade the ruleset major version and the Bazel major version simultaneously.1

That means a new ruleset major should remain compatible with the last Bazel LTS release that the previous major supported.2 If rules_foo 2.x supports Bazel 8 and rules_foo 3.x supports Bazel 8 and 9, users can upgrade the ruleset while staying on Bazel 8, then upgrade Bazel after the ruleset is already ready. If rules_foo 3.x only supports Bazel 9, every user on Bazel 8 has to do both upgrades at once.

A short policy is enough when it answers concrete questions:

Supported Bazel versions:
- Latest Bazel LTS release.
- Bazel at HEAD in CI, used as an early-warning signal.
- Older Bazel LTS releases only while the current ruleset major explicitly says so.

Breaking changes:
- Minor releases stay backward compatible.
- A replacement API is added before the old API is removed.
- Removals happen only in a major release, with migration notes.

The exact versions belong in the ruleset's README, release notes, and CI matrix, not in tribal memory. Consumers will use that information again when they build their own upgrade matrix in M4.1 Ruleset & Toolchain Compatibility Matrix.

The Compatibility Surface Is Bigger Than Rule Names

4.4.1 Rule Public API Design covered the surface of one production rule: attrs, providers, default outputs, output groups, and docs. A published ruleset adds more surfaces:

SurfaceWhat users depend on
Public .bzl entry pointsload("@rules_foo//foo:defs.bzl", "foo_library") keeps working.
Rule attrs and macrosBUILD files keep parsing and mean the same thing.
ProvidersOther rules can still read documented provider fields.
Toolchain typesConsumers can still register and resolve the documented toolchains.
Module extension tagsuse_extension() and tag shapes remain supported.
Generated repo namesRepos exposed through use_repo() keep stable names.
Default outputs and output groupsHumans, CI, IDEs, and packaging rules keep finding expected artifacts.

Do not call something "internal" after users have copied it from examples or docs. If a helper is private, keep it out of public entry points, Stardoc output, examples, and migration snippets. If it must be public, it needs the same evolution plan as a rule attr or provider field.

Bazel Compatibility Is Tested, Not Hoped

A ruleset at HEAD should stay compatible with the latest Bazel LTS release and with Bazel at HEAD. The practical ways to do that are ruleset CI against Bazel at HEAD, enrollment in Bazel downstream testing, and timely fixes when Bazel changes affect the project.2

That is the policy side of 4.5.2 Ruleset Integration Tests and 4.11.5 Ruleset CI/CD Patterns. Integration fixtures prove the public API from a consumer-shaped workspace. CI decides which Bazel versions, platforms, and example workspaces must pass before a tag can be trusted. 4.11.5 Ruleset CI/CD Patterns shows how to implement the release gate. The compatibility policy says what that gate is proving.

Avoid putting required --experimental_* or future --incompatible_* flags on the stable path. Those flags are useful for migration rehearsal, but 3.2.5 Flag Lifecycle is the reason they should not be the only way a normal user can consume a released ruleset.

Some compatibility checks must happen before an ordinary guard could run: directly referencing a Starlark global that does not exist fails while the file loads. bazel_features handles that boundary through bazel_features.globals.<name> and uses named, version-ranged predicates for other capabilities.3 Keep such branches as implementation shims. The support promise still belongs in documentation and the cross-version CI matrix.

Bzlmod Metadata Is A Signal, Not The Whole Policy

4.11.3 Module Metadata & Version Semantics covers the detailed Bzlmod metadata mechanics. The compatibility-policy point is narrower: metadata can warn or check, but it cannot replace readable migration promises. module(..., bazel_compatibility = [...]) lets a module declare Bazel version constraints. Bazel uses that information to check whether the current Bazel version is compatible, but it does not make dependency resolution automatically select a safe ruleset version.4

Be careful with older advice about compatibility_level. Starting with Bazel 8.6.0 and 9.1.0, both compatibility_level and max_compatibility_level are no-ops, and maintainers should stop using them as the way to handle breaking changes.5 If a ruleset makes a breaking change, users need clear failures, release notes, and actionable migration paths. That human-readable policy is still necessary even when module metadata exists.

Deprecate In Public

A good deprecation has four parts:

  1. Add the replacement first.
  2. Document how to migrate.
  3. Keep the old path for a stated window.
  4. Remove it only in a clearly breaking release.

For a provider field, that might mean writing both old_files and files for a release line while consumers migrate. For a module extension tag, it might mean accepting the old tag shape and the new tag shape while warning in docs and release notes. For a generated repository name, it might mean generating both names temporarily, with the old one marked as deprecated.

The important constraint is that the deprecation window has to match the tested matrix. A ruleset cannot promise "old API still works on Bazel 8" if no fixture exercises that old API under Bazel 8. Documentation is part of the contract too: published Stardoc and BCR docs make the supported API discoverable next to the module users install.6

key takeaway

A ruleset compatibility policy should name the supported Bazel range, define the public Starlark and Bzlmod surface, explain how breaking changes are staged, and back the promise with cross-version tests.

Treat P.2.3 Core vs Rulesets as an operational boundary: Bazel Core can evolve, rulesets can evolve, and downstream users should still get a manageable path through both changes.

Check your understanding · 3 questions

1.What makes a ruleset migration manageable for downstream users?

Select one answer

2.Which surfaces should a published ruleset treat as public API when they are documented or used by examples?

Select all that apply

3.True or false: staging breaking changes in a published ruleset.

Choose True or False for each sentence

A new ruleset major should avoid forcing users to upgrade the ruleset major and Bazel major in the same step.
A good deprecation adds the replacement first, documents the migration, keeps the old path for a stated window, and removes it only in a breaking release.
If examples or Stardoc publish a helper, users may reasonably treat it as part of the public compatibility surface.
Required --experimental_* or future --incompatible_* flags are a good stable consumption path for ordinary release users.
0 of 3 answered

Footnotes

  1. Rule Compatibility - manageable migration means users are not forced to upgrade both the ruleset major version and Bazel major version simultaneously. 1 2

  2. Rule Compatibility - best practices for SemVer, latest LTS compatibility, Bazel-at-HEAD compatibility, downstream testing, bazel_features, and new-major compatibility with the previous major's last supported Bazel LTS. 1 2

  3. bazel_features repository mapfeatures.bzl catalogs version-ranged predicates, while private/globals.bzl records boundaries for globals that cannot be probed by directly loading them.

  4. MODULE.bazel files - bazel_compatibility checks current Bazel compatibility and does not affect dependency resolution.

  5. Frequently asked questions - compatibility_level / max_compatibility_level no-op status starting with Bazel 8.6.0 and 9.1.0. Breaking changes should provide clear errors and migration paths.

  6. Bazel Starlark Docs on the Registry - starlark_doc_extract, docs_url, and BCR-hosted documentation for published Starlark APIs.