4.11.1 Ruleset Layout & Public Entry Points
A reusable ruleset has two audiences: Bazel, which loads the .bzl files, and humans, who decide what is safe to copy. The repository layout is part of that API. It tells users which files are public entry points, which setup snippets are supported, and which helpers are internal implementation detail.1
Repository layout communicates the support contract.
Documented and versioned. Only usage snippets are copied.
Undocumented, movable, refactorable
The Repository Teaches The Contract
The official guidance starts with a naming convention: publish reusable rules in their own repository, commonly named $ORGANIZATION/rules_$NAME, with a clear repository description and top-level README.2 That is not just cosmetics. A ruleset is usually found through a registry, GitHub, or a copied bazel_dep() snippet. The name and README are the first compatibility signal users see.
At the top level, MODULE.bazel defines the module name users depend on. The deploying guide's example keeps that name stable and shows the release announcement snippet users should paste into their own MODULE.bazel:
bazel_dep(name = "rules_mylang", version = "1.2.3")
Changing the module name after users adopt it is therefore a migration event, not a refactor. 4.11.3 Module Metadata & Version Semantics explains the detailed version and compatibility semantics, but the layout should already make the dependency entry point obvious.
Public .bzl Files Are Doorways
A ruleset may contain many .bzl files, but users should not have to guess which ones are safe to load. The deploying guide recommends a language or domain package with a defs.bzl entry point that exports all public rules.3 In practice, a mature ruleset often has several public doorways:
The bazel-contrib scaffold demonstrates that boundary in one production-shaped tree: its mylang/ package keeps defs.bzl, extension, repository, and toolchain entry points above a clearly separate private/ implementation directory.4 Treat its sample rule and smoke target as placeholders. The repository is evidence for maintainable layout and release plumbing, not a tutorial for implementing a language rule.
mylang/
defs.bzl # rules, macros, public providers
extensions.bzl # Bzlmod module extensions
repositories.bzl # legacy WORKSPACE setup during migration
The exact filenames are illustrative. What matters is that the supported path is documented, stable, and easy to find. If users need mylang/private/toolchain_impl.bzl to make the first example work, the ruleset has leaked an implementation file into its public API.
A minimal version of this pattern keeps implementation symbols under internal/, while def.bzl loads and re-exports the public go_binary symbol so downstream users do not need to update their load() statements when internals move.5 The same idea extends to providers: GoLibraryInfo and other provider symbols are public API when compatible downstream rules need to consume or implement them.6
This continues the rule-level API design from 4.4.1 Rule Public API Design. There, attrs, providers, outputs, and docs become stable faces of one rule. Here, the public .bzl entry points become the stable face of the whole ruleset: they decide which rule symbols, provider symbols, module extensions, and setup helpers downstream code may rely on.
Internals Need A Place To Move
Private implementation files are not bad. They are how a ruleset stays maintainable. Put action-building helpers, low-level repository rules, generated-version tables, toolchain implementation details, and provider plumbing behind private paths or clear naming conventions. Users can still read the source, but the layout should make unsupported loads feel obviously unsupported.
That separation gives maintainers room to reorganize internals without breaking every downstream load() statement. Public entry points can re-export stable symbols while private files split, merge, or move. The compatibility policy in 4.11.6 Rule Compatibility & Bazel Version Policy should define how public attrs, providers, generated repo names, and setup helpers evolve. Internal helper names should not need the same promise.
Setup Is Part Of The Layout
Rulesets do not only publish rule symbols. They also publish setup paths: bazel_dep(), module-extension tags, use_repo() names, toolchain registration, and sometimes legacy WORKSPACE macros while users migrate. Keep those setup files near the public API and document the intended order.
Toolchains make this especially visible. The deploying guide notes that registered toolchain() targets are always analyzed during toolchain resolution, while the implementation targets behind their toolchain attribute are only needed when that toolchain is actually selected. It recommends splitting the repository that contains toolchain declarations from the repository that contains heavier implementation targets when registration would otherwise force unnecessary fetching.7 That design becomes the toolchainization pattern in 4.11.2 Toolchainization.
Release artifacts add one more boundary: the source repository layout and the distributed archive do not have to be identical. A common release technique uses git archive plus .gitattributes to stamp version files, omit bulky folders such as examples from the shipped archive, and keep release creation tied to a tag.8 4.11.4 Publishing Rulesets develops the release mechanics. The layout decision here is which directories are source-only support material and which are part of the published module.
Examples And Docs Prove The Public Path
Examples and tests should exercise the public entry points, not private shortcuts. The official layout includes top-level tests/ and optional examples/ directories, with examples showing basic ways users can apply the rules.9 The executable-documentation side was covered in 4.5.3 Example Workspaces as Contract Tests: an example workspace should look like a downstream consumer, load public .bzl files, and run the commands the README teaches.
Generated API documentation closes the loop. Public .bzl files and public functions should carry docstrings, while rules, aspects, attributes, providers, and provider fields should use doc metadata so documentation tools have structured API text to extract.10 The newer BCR documentation flow lets rulesets publish generated Starlark docs with the module via docs_url metadata.11 Keep the docs close to the entry points they describe, and make CI check that the published docs and examples still match the public surface. 4.5.4 Stardoc — API Documentation explains generation, and 4.11.5 Ruleset CI/CD Patterns turns it into a release gate.
A ruleset layout should answer three questions without a maintainer in the room: what should users load, how should they enable the ruleset, and which files are implementation detail?
Put public symbols behind documented entry points such as defs.bzl and extension setup files. Keep private helpers out of the supported load path. Then use examples, tests, and generated docs to prove that the documented public path works.
Check your understanding · 3 questions
1.Match each ruleset path to the role it should usually play.
Drag each answer onto the matching prompt, or click an answer and then click a prompt
2.Which choices help keep a reusable ruleset's public API stable?
Select all that apply
3.True or false: public and private boundaries in ruleset layout.
Choose True or False for each sentence
Footnotes
-
Deploying Rules — repository layout,
MODULE.bazel, README,defs.bzl, tests, examples, and docs as the reusable ruleset surface. ↩ -
Deploying Rules — hosting and naming guidance for
$ORGANIZATION/rules_$NAME, repository descriptions, tags, and README titles. ↩ -
Deploying Rules — language/domain package containing
BUILDand publicdefs.bzlentry point. ↩ -
rules-template repository map — public entry points, private helpers, consumer-shaped smoke tests, toolchain repositories, and release automation in the maintained scaffold. ↩
-
Writing Bazel rules: simple binary rule — public
def.bzlre-exportsgo_binaryfrominternal/rules.bzlto keep userload()statements stable. ↩ -
Writing Bazel rules: library rule, depsets, providers — public
def.bzlre-exports both rules andGoLibraryInfofor compatible downstream rules. ↩ -
Deploying Rules — toolchain registration can force analysis/fetching of toolchain targets. Split declaration repos from heavy implementation repos when needed. ↩
-
Publishing Bazel rules that depend on tools: take 2 —
git archive,.gitattributes, version stamping, excluding examples, and inserting release integrity hashes. Releasing Bazel rulesets that publish tools — tag-triggered release automation and source-vs-release archive behavior. ↩ -
Deploying Rules — top-level
tests/and optionalexamples/directories for verifying and demonstrating reusable rules. ↩ -
.bzl style guide — docstrings for public files/functions and
docmetadata for rules, aspects, attrs, providers, and provider fields. ↩ -
Bazel Starlark Docs on the Registry —
starlark_doc_extract,bzl_library,docs_url, and BCR rendering for published Starlark API docs. ↩