Migrating Gerrit and Plugins to Bazel Modules

Outline

  • Bazel Modules (Bzlmod) overview

  • Version support for Bazel Modules

  • Migrating Gerrit and JGit

  • Migrating Gerrit plugins

  • Current status and lessons learned

Bzlmod Overview

  • Introduced by the Bazel team around 2020

  • Replaces WORKSPACE with MODULE.bazel

  • Eliminates manual load() dependency wiring

  • Improves reproducibility and dependency resolution

  • Uses the Bazel Central Registry (BCR)

Version Support

  • Gerrit currently uses Bazel 7.6.1

Bazel 7.x (2024)

  • Full support for Bazel Modules

  • Hybrid mode still supported

    • WORKSPACE

    • WORKSPACE.bzlmod

    • MODULE.bazel

Bazel 8.x (2025)

  • --enable_workspace is disabled by default

  • Legacy WORKSPACE builds require an explicit opt-in

Bazel 9.x (2026)

  • Hybrid mode (WORKSPACE.bzlmod) support is removed entirely

  • --enable_workspace is now a no-op

  • All projects must use MODULE.bazel

  • Legacy dependencies must be handled via module extensions

JGit and Gerrit Migration

  • Migration initiated by the JGit team in late 2025

  • JGit migrated first using RJE (jgit_deps)

  • Gerrit followed with gerrit_deps

  • Result: two independent dependency graphs

Problem: Version Skew

  • Different versions of the same dependencies were being pulled in

  • Multiple versions were included in release.war

  • Increased artifact size and runtime conflicts (classpath ambiguity)

Unified Dependency Graph

Disadvantage of Unified Graph

Show Bazel Modules

  • bazel mod graph shows the resolved dependency graph

$ bazel mod graph
<root> (gerrit@_)
├── bazel_features@1.39.0
├── rules_java@8.16.1
├── jgit@_
└── protobuf@33.4

MODULE.bazel.lock

  • Must be updated whenever MODULE.bazel changes

  • Usually updated automatically during builds

  • Important: Always commit the updated lock file

$ bazelisk mod deps --lockfile_mode=update

Enforce Strict Lock Sync

  • Suppress automatic lock updates (CI verification)

  • Prevents desync between MODULE.bazel and MODULE.bazel.lock

  • Common idiom to enforce strict lock mode in .bazelrc

common --lockfile_mode=error

Structuring MODULE.bazel

  • MODULE.bazel uses include() to improve maintainability

module(name = "gerrit")

include("//tools:bazlets.MODULE.bazel")
include("//tools:java_deps.MODULE.bazel")
include("//plugins:external_plugin_deps.MODULE.bazel")

Library Compliance

  • Conducting a release should not require LC+1

  • version.bzl removed

  • GERRIT_VERSION moved to tools/bazlets.MODULE.bazel

Managing Dependencies

  • Use the rules_jvm_external extension

maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")

maven.install(
    name = "external_deps",
    lock_file = "//:external_deps.lock.json",
)

Two Different TOML Files

  • Two different files are used (Library Compliance):

    • tools/deps.toml — dependencies with LC requirement

    • tools/nongoogle.toml — dependencies without LC requirement

Updating Dependencies

  • Update the version in the toml file

  • Regenerate the lock file: external_deps.lock.json

$ bazel build :headless
REPIN=1 bazel run @external_deps//:pin

Migrating from maven_jar

  • maven_jar is replaced with rules_jvm_external

  • Support for transitive dependencies: both a feature and a problem

Legacy (WORKSPACE)

  • transitive dependencies must be manually specified

maven_jar(
  name = "openid-consumer",
  artifact = "org.openid4java:openid4java:1.0.0",
)
maven_jar(
  name = "nekohtml",
  artifact = "net.sourceforge.nekohtml:nekohtml:1.9.10",
)

Modern (rules_jvm_external)

  • Transitive dependencies are resolved automatically

[libraries]
openid-consumer = { module = "org.openid4java:openid4java", version = "1.0.0" }

Key Improvement

  • Transitive dependencies are now resolved

$ bazelisk query "deps(@external_deps//:org_openid4java_openid4java)"
  @external_deps//:net_sourceforge_nekohtml_nekohtml

Unwanted Transitive Dependencies

  • Extra dependencies can be pulled into release.war unintentionally

  • Applies to both core Gerrit and plugins

Detecting Dependency Changes

  • Avoid shading new dependency unintentionally in release.war

  • Track release.war contents via an allowlist

bazel test //Documentation:check_release_war_jars

Extending release_war_jars.txt

  • If the contents change:

bazelisk build //:release.war.jars.txt
cp bazel-bin/release.war.jars.txt Documentation/release_war_jars.txt

Inspect @external_deps

  • Use the query command to see resolved dependencies:

bazelisk query "deps(@external_deps//:all)"
@external_deps//:commons_logging_commons_logging

Use somepath Query Predicate

bazelisk query "somepath(:release, @external_deps//:commons_logging_commons_logging)"
//:release
//java/com/google/gerrit/httpd/init:init
//java/com/google/gerrit/httpd/auth/openid:openid
//lib/openid:consumer
@external_deps//:org_openid4java_openid4java
@external_deps//:commons_logging_commons_logging

Two Plugin Build Modes

  • Standalone plugin build mode: build outside of the Gerrit workspace

  • In-tree build mode

  • Major macros were moved to bazlets to harmonize build modes

Standalone Build Mode

  • Examples are the oauth and javamelody plugins

  • After migration to Bazel Modules, it is easy to support standalone build mode

  • Outside the scope of this presentation

Migrating Plugin with External Dependencies

  • Is the plugin affected by the Bzlmod migration?

  • Create MODULE.bazel

  • Consume external dependencies with rules_jvm_external

  • Migrate external_plugin_deps.bzl to a MODULE fragment

  • Optional: implement tests for allowlist and Gerrit overlap

Is the Plugin Affected by Bzlmod Migration?

  • Replace provided Gerrit dependencies from the maven_jar repository:

  deps = ["@commons-io//jar"],

to rules_jvm_external repository and coordinates:

  deps = ["@external_deps//:commons_io_commons_io"]

Alternative Addressing of Dependencies

  • Use lib/<dependency> wrapper rule instead:

  deps = ["//lib/commons:io"]

Avoid Specifying Dependencies Exposed in the Plugin API

  • commons-compress was bumped to version 1.28.0

  • commons-io is a transitive dependency of commons-compress

  • Therefore, commons-io is exposed in the Plugin API

  • The explicit dependency can be removed entirely

Javamelody: Example for Plugin Migration to Bzlmod

Create MODULE.bazel

module(name = "gerrit-javamelody")
bazel_dep(name = "rules_jvm_external", version = "6.10")

maven.install(
    name = "javamelody_plugin_deps",
    artifacts = [
        "net.bull.javamelody:javamelody-core:1.99.3",
    ],
    lock_file = "javamelody_plugin_deps.lock.json",
    repositories = [
        "https://repo1.maven.org/maven2",
    ],
)

Adapt BUILD to Use Bazlets

  • gerrit_plugin and gerrit_tests macros are consumed from the bazlets repository

  • The interface was not changed

load(
    "@com_googlesource_gerrit_bazlets//:gerrit_plugin.bzl",
    "gerrit_plugin",
    "gerrit_plugin_tests",
)

Adapt BUILD to New Repository and Coordinates

    deps = [
        "@javamelody_plugin_deps//:net_bull_javamelody_javamelody_core",
    ],

Add Allowlist Test

load(
    "@com_googlesource_gerrit_bazlets//tools:runtime_jars_allowlist.bzl",
    "runtime_jars_allowlist_test",
)
runtime_jars_allowlist_test(
    name = "check_javamelody_third_party_runtime_jars",
    allowlist = ":javamelody_third_party_runtime_jars.allowlist.txt",
    hint = ":check_javamelody_third_party_runtime_jars_manifest",
    target = ":javamelody__plugin",
)

Add Gerrit JAR Overlap Test

load(
    "@com_googlesource_gerrit_bazlets//tools:runtime_jars_overlap.bzl",
    "runtime_jars_overlap_test",
)
runtime_jars_overlap_test(
    name = "javamelody_no_overlap_with_gerrit",
    against = "//:headless.war.jars.txt",
    hint = "Exclude overlaps via maven.install(excluded_artifacts=[...]) and re-run this test.",
    target = ":javamelody__plugin",
    target_compatible_with = in_gerrit_tree_enabled(),
)

Create MODULE Fragment

  • Add external_plugin_deps.MODULE.bazel with this content:

maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")

bazel_dep(name = "gerrit-javamelody")
local_path_override(
    module_name = "gerrit-javamelody",
    path = "plugins/javamelody",
)

use_repo(maven, "javamelody_plugin_deps")

Remove external_plugin_deps.bzl

  • After the migration, external_plugin_deps.bzl should be removed.

Done

CI Support: Zuul and GerritForge CI

  • Zuul was extended to support: external_plugin_deps.MODULE.bazel

  • GerritForge CI was extended to support: external_plugin_deps.MODULE.bazel

  • Seamless support for migration from maven_jar to Bazel Modules

  • Upload migration change to Bazel Modules: Verify+1

Key Takeaways

  • Migration is mandatory for Bazel 9+

  • Dependency unification is critical to avoid bloat

  • Lock files must be maintained carefully

  • Transitive dependencies require active monitoring

Lessons Learned

  • Start with a single dependency graph early in the process

  • Avoid creating parallel dependency trees

  • Automate the verification of final artifacts

  • Invest in tooling around lock file maintenance

Status

  • JGit migration is complete

  • Gerrit core migration is in progress

  • PolyGerrit migration is ongoing

  • Plugin migration is ongoing

  • Tooling is still evolving

Acknowledgements

  • Bazel team — for designing and implementing Bzlmod

  • Ivan Frade — for migrating JGit to Bzlmod

  • Thomas Dräbing — for migrating Gerrit, Bazlets, PolyGerrit and plugins to Bzlmod

  • Matthias Sohn, Milutin Kristofic, and Luca Milanesio — for reviews and feedback