6.7.3 Automating Bazel Dependency Updates

A dependency bot can change one version and open a pull request in seconds. That does not mean it completed a Bazel dependency update. The real unit of change is a coherent repository state: declarations, Bazel and ecosystem lockfiles, patches, vendored or generated files that are meant to be committed, and the validation evidence for every supported configuration.

Treat the update as a transaction

Begin by defining the transaction for each dependency integration. The exact files differ by ruleset and language, but the roles are stable:

RoleTypical repository stateQuestion the updater must answer
Declared intentMODULE.bazel, extension tags, ecosystem manifests, version catalogsWhich dependency or related group is supposed to change?
Resolved stateMODULE.bazel.lock and ecosystem lockfilesWhich resolver regenerates each file, and which host OS/architecture or ecosystem dimensions affect it?
Source adaptationpatches and patch declarationsDoes the new source still accept every carried patch?
Checked-in materialvendor directories or ruleset-generated manifests that the repository deliberately versionsWhich owning command refreshes it, and how is drift detected?
Validation evidencebuild and test results for the protected scopeWhich states and configurations were actually exercised?

The table is a discovery aid, not a universal Bazel file list. A repository may have one lock layer or several. 3.1.12 Layered Ecosystem & Bzlmod Lockfiles explains why MODULE.bazel.lock and an ecosystem lock can record outputs from different resolvers and must be regenerated by their respective owners. Bazel's lockfile records module-resolution and module-extension state. When an extension declares that its evaluation depends on the host operating system or architecture, Bazel can keep separate entries for those host dimensions.1 Those are not target- platform entries. The update contract must name two independent coverage axes: the hosts on which repository evaluation and lock regeneration must run, and the target configurations and platforms whose builds and tests must pass. That makes “the lockfile changed” insufficient evidence that every relevant lock layer and validation axis was refreshed.

Keep materialized external repositories and repository caches out of the commit transaction unless a particular integration explicitly defines a checked-in artifact. They are execution state, not automatically source-controlled update outputs. 6.7.1 Serving Dependencies Inside the Organization separates registries, mirrors, repository caches, remote downloaders, and vendor mode; the updater must preserve the dependency identities and integrity expectations that those services deliver, not mistake their transient contents for reviewable repository state.

Write an update contract for each integration before automating it:

trigger inputs -> declarations changed -> owner commands run
               -> expected files changed -> forbidden drift absent
               -> validation scope complete -> reviewable commit

Every arrow needs an observable result. If the updater recognizes a declaration but cannot name its regeneration command or expected outputs, its result is unsupported, not success.

Separate the trigger from Bazel-aware orchestration

Renovate, Dependabot, or a custom scanner can detect that a newer version exists and propose an edit. Detection is only the first stage. A large-monorepo case study used a longer pipeline: detect candidates, rank them, select and group an update, create a change, compute affected targets, validate those targets, and publish the resulting artifacts.2 The durable lesson is the separation of responsibilities, not that every repository should copy that product or its ranking algorithm.

The Bazel-aware orchestrator takes the proposal and completes the transaction:

  1. Identify the dependency integration and all files it controls.
  2. Apply the intended declaration change, including related versions that must move together.
  3. Run that integration's pinned resolution and generation workflow from clean repository state.
  4. Compare the resulting diff with the integration's declared output set.
  5. Reject missing outputs, unexpected generated changes, dirty state after a verification rerun, and integrations the automation does not understand.
  6. Validate the resulting repository state across its protected configurations.
  7. Publish one reviewable change containing the declaration, regenerated state, patches, and evidence identifiers.

Do not let a generic updater hand-edit generated files to make the diff look complete. Regeneration is evidence that the owning resolver accepts the new declaration. A second regeneration that leaves the worktree unchanged is a useful drift check: it shows the proposed commit is a fixed point for the tested workflow rather than half of a local transformation.

Grouping is part of the contract. The case study grouped dependencies from the same family when they targeted the same version, preventing the automation from treating coupled changes as unrelated proposals.2 In another repository the coupling might come from a shared BOM, a toolchain/compiler pair, or patches that apply only to one version family. Record the reason for the group; do not infer atomicity from similar names alone.

Make partial regeneration fail visibly

A pipeline is unsafe when it reports only whether its commands exited zero. Consider an update that changes an extension tag and an ecosystem manifest but regenerates only MODULE.bazel.lock. A build on one host might remain green while a required host-OS-/architecture-specific extension entry, generated manifest, or patch is stale. Independently, the default target configuration can remain green while a protected nondefault configuration is broken.

Use a clean-state fixture to test the failure path:

  1. Start from the committed revision without a warm output base or hidden local resolver state.
  2. Apply a known update whose contract requires at least two related outputs.
  3. Deliberately omit one regeneration step.
  4. Run the automation's completeness check.
  5. Require an explicit failed or uncertain result before build validation can be reported as green.

Bazel's lockfile contains extension state reached by current or previous invocations, and host-dependent extensions may have entries keyed by operating system, architecture, or both.1 Consequently, one invocation on one host is not proof that every required host-specific entry has been refreshed. Regenerate on the host dimensions named by the contract, and separately build and test its protected target configurations and platforms. Then run the lockfile or drift enforcement mode chosen by the repository. Avoid copying a single command into every integration: the owning ruleset determines how its ecosystem lock or generated manifest is updated.

The runnable dependency update transaction turns this orchestration boundary into a deterministic fixture. Its checked-in cases prove a complete idempotent update and reject partial regeneration, an unsupported bot form, a default-green but nondefault-broken candidate, and a non-idempotent generator. The JSON lock layers are deliberately a repository contract model, not fabricated MODULE.bazel.lock contents or a simulation of Bazel's resolver; a real integration replaces them with its owning regeneration and validation commands.

The same fixture should cover patches. Upgrade the source to a version for which a carried patch no longer applies, and require the transaction to fail before a partial commit is offered. If the updater intentionally removes or rewrites the patch, that decision belongs in the same diff and review.

Validate impact without turning uncertainty green

Dependency changes can affect many targets that do not mention the edited file directly. The case study used bazel-diff to narrow build and test work, but it also identified test failures versus CI flakiness as an unresolved operational challenge.2 A selector and a retry policy therefore need explicit safety contracts; neither is proof merely because the dependency bot created a PR.

Use 6.5.6 Fail-Closed Target Selection for the selector boundary. An affected result may route to the returned targets inside the declared universe. An unknown, stale, incompatible, or unsupported result must route to the protected fallback. Preserve the dependency transaction identity, base and proposed revisions, configurations, selector version, selected scope, fallback decision, and terminal results together.

Test one intentionally bad update outside the default configuration. The automation passes only when the protected configuration fails or the selector admits uncertainty and the fallback discovers the failure. A green default build does not override missing configuration evidence.

Retries need the same discipline. Retry a job only under a classified transient failure policy and retain every attempt. A failed test must not become “flaky” solely because retrying is convenient; an unavailable log or incomplete result is uncertainty. The update stays blocked until the required evidence is complete or an accountable human makes an explicit exception.

Roll back the repository state, not the version string

The rollback unit is the full accepted transaction. Preserve its parent commit and either revert the entire update or generate a new coherent transaction that restores the prior dependency identity. Reverting only MODULE.bazel while leaving lockfiles, patches, vendor contents, or ruleset-generated manifests from the newer version creates a mixed state that may fail later or mislead bisection.

Keep artifact publication outside the update's success criterion. The case study included publication as a later pipeline stage,2 but a failed publication must not mutate or reinterpret the validated repository transaction. The update record should identify which immutable build outputs were produced; release eligibility and side-effecting publication have their own boundaries in 6.7.4 From Build to Release Candidate and 6.7.10 Publishing Artifacts Reliably.

Finally, test the bot adapter itself. Feed it one supported declaration and one plausible unsupported form. The supported case should produce the complete, idempotently regenerated diff. The unsupported case should preserve the prior committed state and report why it cannot complete the transaction. The fixture's transaction tests compare the accepted state before and after every rejected case, making coherent rollback observable. This is the difference between a useful automation trigger and an updater that silently normalizes uncertainty into green.

key takeaway

A Bazel dependency update is one transaction across declared intent, every committed resolver or generated output, patches and vendored material that the integration controls, and complete validation evidence. Repository caches and materialized external repositories are separate ephemeral state.

Let a bot propose the change, then require a Bazel-aware contract to regenerate the owning integrations, prove an idempotent clean-state diff, fail visibly on partial or unsupported cases, and validate a conservative configuration scope. Rollback or bisect the whole transaction, never only its version string.

Check your understanding · 4 questions

1.An updater changes an extension tag, regenerates MODULE.bazel.lock, and gets a green default build. The integration contract also requires an ecosystem lockfile, but that file still describes the old version. What should the updater report?

Select one answer

2.Match each coverage question to the evidence that answers it:

Drag each answer onto the matching prompt, or click an answer and then click a prompt

Answers
Did a host-dependent module extension refresh all required lock entries?
Does the candidate work for every protected target configuration?
Can the bot adapter understand the proposed declaration form?
Does regeneration reach a stable repository state?

3.Which conditions support accepting a dependency update transaction?

Select all that apply

4.A proposed version passes the default configuration but fails a protected nondefault configuration. Which rollback behavior preserves a coherent repository state?

Select one answer

0 of 4 answered

Footnotes

  1. Bazel Lockfile — lockfile generation, module-resolution state, extension inputs and generated repository specifications, and platform-specific extension entries 1 2

  2. How We Automated 3rd-Party Dependency Management in a Bazel Monorepo - Aditya Aggarwal, Salesforce — staged update architecture, grouped candidates, affected-target validation, and retry-versus-failure limits 1 2 3 4