gate
Registry admission gating for tomato-bazel: prove invariants about a module from its graph before it enters the registry.
| Latest | 0.0.1 |
|---|---|
| Versions | 1 |
| Category | Modules & tooling |
| Maintainers | Matt Marshall |
| Registry | https://registry.tbzl.dev/modules/gate/ |
| Source | github.com/tomato-bazel/gate |
bazel_dep(name = "gate", version = "0.0.1")
View source & releases on GitHub ↗
Registry admission gating for tomato-bazel: prove invariants about a module from its graph before it is allowed into the registry.
Status: v0.0.1 — M1 done, M2 started
bzlmod (MODULE.bazel evaluation, module graph, MVS) is complete and verified
against Bazel’s own resolution. Four gates run as Bazel targets:
bazel test //gates/....
Why
bazel-registry publishes 74 modules / 314 versions and nothing validates an
entry. rels release computes the SRI itself and it is never re-verified;
there is no PR-triggered workflow in the registry at all. The cost shows up as
reactive yanking — rules_cc_host 0.1.0 (“cannot work as a bazel_dep”),
rules_lang 0.4.2 (“breaks consumer package loading”). Both are failures a
graph invariant catches at publish time.
Meanwhile the MODULE.bazel files in rules_k8s and rules_lean already carry
hand-written proofs of exactly these invariants, in prose. This repo makes them
machine-checked.
Design
Rust does extraction only. Every gate is a Bazel target.
MODULE.bazel + *.bzl + source.json + tarballs
│
[Rust] tbzl-gate project → facts.ttl
│
rdf_dataset(...)
│
┌────────────┴────────────┐
sparql_query_test(*.rq) rdf_validate_test(shapes)
one target per invariant the convention tier
└────────────┬────────────┘
│
[Rust] tbzl-gate certify → certificate.binpb (signed)
rules_rdf already provides the primitive: sparql_query_test is a zero-row
gate — run a .rq and fail the build if the result set is non-empty. So a
gate is one query file plus one target, the violating rows are the
counterexample, and adding a gate needs no Rust rebuild.
Transitive closure comes from SPARQL property paths (gate:depends_on+,
gate:loads*) rather than an iterated fixpoint — the capability transfer
function is pure union along load edges, so cap(f) = ⋃ { own(g) : f loads* g }
is a single query.
Crates
| Crate | Purpose |
|---|---|
bzlmod | MODULE.bazel evaluation, the module graph, MVS, registry client |
gate-rdf | Projection to Turtle. Facts only — no policy |
gate-report | Gate results → tomato.gate.v1.ConformanceReport. Projects; decides nothing |
gate-snapshot | Upstream registry metadata snapshots, for --check drift |
The report is a proto, not a rendered document: it crosses into
tomato-bazel/docs, which builds docs.tbzl.dev/conformance/ from it with no
Bazel and no JVM. Publishing //gates:report_bundle ships the message alongside
its own FileDescriptorSet, so the consumer needs neither a copy of the schema
nor a generated client.
bzlmod is also roma’s unstarted B1 (roma/docs/bzlmod-strategy.md: own
parse + MVS + registry fetch). Building it here means the registry gets gating
now and roma gets bzlmod resolution from the same implementation — and it
retires the three divergent regex parsers of MODULE.bazel currently in
bazel-registry/tools/graph/graph.py, modgraph-operator, and tbzl.
Reconnaissance
cargo run -p bzlmod --example survey reports what the S/C/D/V gates would
find across the registry today. It reports; it does not judge. Current output
over the latest version of each of the 74 modules:
| Gate | Finding |
|---|---|
| S4 | 5 local_path_override in published modules |
| C1 | 10 of 30 external atoms pinned at more than one version |
| D2 | 32 of 74 modules register toolchains that propagate to every consumer |
| D3 | 34 repo names chosen on a shared extension without a module namespace |
| V1 | dropped — compatibility_level is a no-op as of Bazel 8.6.0 / 9.1.0 |
The gates themselves run over the projection
(bazel run //gates:report_<gate>), and currently find: S4 5, D2 58
registrations across 31 modules, D3 35, C1 26 version pairs over 19 contested
atoms, S6 1 — 125 findings across 5 invariants, none of them clean, over 76
modules of which 39 have at least one finding.
Those numbers are published, not just printed. bazel build //gates:report
projects them into a tomato.gate.v1.ConformanceReport, and
docs.tbzl.dev/conformance/ renders it —
the scoreboard, a page per invariant with its rationale taken from the .rq
header, the third-party atom frontier, and each module’s findings on that
module’s own reference page.
compatibility_level deserves a note, because it changes what C1 is for.
Bazel made it and max_compatibility_level no-ops, so there is no longer any
in-band way for a module to signal a breaking change: MVS takes max-of-minimums,
and if A needs x 1.0.0 while B needs x 2.0.0, A silently gets 2.0.0. Bazel
used to reject that and cannot now. One live version per atom is therefore the
compatibility mechanism, not merely a tidiness goal — it is the only thing
standing between a consumer and a silent breaking upgrade. The registry has 9
such silent upgrades today, including botnoc declaring rules_java 8.15.2 and
receiving 9.1.0.
D3 is the one worth reading closely, because the naive form of it is wrong.
Apparent repo names are module-scoped, so @crates in two unrelated modules is
not automatically a conflict. The hazard is narrower: a name the module chose
via a tag (oci.pull(name = ...)) on an extension defined in another module,
since extension instances — and the repos they generate — are shared across the
graph. That is the bug rules_k8s hit on its first consumer, and the fix it
shipped (prefix with the module name) is what this checks. Singletons the
extension emits for everyone, like rust_toolchains, are correctly ignored.
Install
.bazelrc:
common --registry=https://registry.fastverk.com/
common --registry=https://bcr.bazel.build/
MODULE.bazel:
bazel_dep(name = "gate", version = "0.0.1")Conformance#
No gate findings. 8 contested atoms. See how gating works or the full report.
Contested atoms
Third-party modules where this module resolves a different version than others do. Not a violation of anything this module did — it is the actionable form of a registry-level convergence finding, and the sentence a maintainer can act on.
| Atom | Resolved here | Elsewhere |
|---|---|---|
apple_support | 1.24.2 | 2.2.0 ×1 |
bazel_skylib | 1.8.2 | 1.9.0 ×2 |
nlohmann_json | 3.6.1 | 3.12.0.bcr.1 ×1 |
protobuf | 33.4 | 34.0.bcr.1 ×2 |
rules_jvm_external | 6.7 | 6.8 ×4 |
rules_python | 1.7.0 | 2.0.1 ×1 |
rules_swift | 3.1.2 | 3.6.1 ×1 |
upb | 0.0.0-20220923-a547704 | 0.0.0-20230516-61a97ef ×1 |
Dependencies#
Depends on
Versions#
1 published version, newest first. Each resolves to an immutable, integrity-checked archive.
| Version | Integrity (sha256) | Source archive |
|---|---|---|
0.0.1 latest | A05NaZjwvvo7dETw… | tag archive ↗ |
Changelog#
All notable changes to gate. The format is loosely Keep a Changelog — version headers mirror the published bazel-registry entries.
Unreleased
Added
-
bzlmodcrate:MODULE.bazelevaluation over starlark-rust in a restricted dialect (nodef, noload), replacing regex scraping. Captures the directives the gates need:dev_dependency,register_toolchains,use_repo, every*_override, andcompatibility_level. -
bzlmod::Version: bzlmod version ordering per upstreamVersion.java, including the digit-vs-alphanumeric prerelease precedence that both ofrels’ hand-rolledversion_cmpfunctions get wrong. -
Corpus test over all 314 registry entries.
-
--example survey: reconnaissance over the registry for the S/C/D/V gates. -
bzlmod::registry: BCR-layout registry reader (FsRegistry),source.json/metadata.jsonmodels includingyanked_versions. -
bzlmod::graph: the module graph and minimal version selection. Discovery and selection iterate to a fixed point, because bumping a module to a higher version means reading that version’sMODULE.bazel, which may declare deps the lower version never had.dev_dependencyedges are followed only from the root — the mechanism that makes a dev dep invisible to consumers, and the basis of D1. -
ModuleGraph::downgraded_requirements: modules that asked for one version and silently got a higher one. Sincecompatibility_levelbecame a no-op in Bazel 8.6/9.1 this is the only remaining signal for that class of break. -
--example selections: prints MVS results as JSON for diffing against Bazel’s own answer. Verified againstrules_k8s,rules_leanandrules_tlalockfiles: zero mismatches on every module the registry can resolve. -
Bazel build:
bazel test //...alongsidecargo test, over @gate_crates. -
gate-rdf: projects the registry to Turtle. Facts only, no policy. -
//gates: the first four invariants assparql_query_testtargets — S4 (no override in a published module), D2 (undeclared toolchain leakage), D3 (unnamespaced repo on a shared extension), C1 (one live version per atom). Each is wired both against a clean fixture (must return zero rows) and a known-violation fixture diffed against a committed golden, so a malformed query cannot masquerade as a passing gate. -
gate-report+//gates:report: the conformance report — a scoreboard, the findings per invariant, the findings per module, and the third-party atom frontier with BOM conformance. Regenerated from the projection on every build. Gate rationale is EXTRACTED from each.rqfile’s header comment rather than written separately, so documentation cannot drift from the gate it documents. Findings are attributed to a module only when the query exposes a?modulevariable; registry-level invariants like C1 stay on the report as a whole rather than being blamed on an arbitrary module. -
//proto/tomato/gate/v1:conformance_proto— the report’s wire contract, and the new shape of//gates:report. It emits a singletomato.gate.v1.ConformanceReport(report.binpb) plus its ownFileDescriptorSet(conformance-descriptor.binpb), so a consumer can decode it without a copy of the schema.//gates:report_bundleis the pair.This REPLACES the mdBook source tree the report used to be. Rendering moved to tomato-bazel/docs, which publishes docs.tbzl.dev/conformance/ — one site, one theme, one search index, and a module’s findings sitting on the same page as its
bazel_depline instead of in a parallel book. The report is now data; the editorial framing lives where editorial things belong.Provenance (
generated_at,generated_from) arrives viastring_flag, so a localbazel build //gates:reportis byte-reproducible and an unstamped report is visibly unstamped rather than quietly claiming to be current.The prost toolchain (
//proto:prost_toolchain) is tonic-free — the default one wirestonic_runtimeinto everyrust_prost_library, and nothing here speaks gRPC — and is registereddev_dependency = True, because a non-devregister_toolchainsis exactly the D2 leak this repo detects.