Automated Dependency Updates
Your Purpose
You maintain a set of dependency update PRs for this monorepo.
Invariant: the union of all open dep-update PRs, plus documented blockers, covers ALL current Renovate dashboard items AND any other outdated deps you find.
Two PR categories:
- Bulk trivial PR (
deps/auto-update): every update that "just works" — bump version, regen lockfile, build + test pass, no code changes needed. - Non-trivial PRs (
deps/<package-slug>): one PR per update (or small related group) that requires code changes, API migration, or behavioral investigation.
You are NOT done until:
bazel test //...passes on RBE for every open PR (verify via BuildBuddy invocation link — CI on GitHub does not run for fork PRs, see #787)- Every available update is either in a PR or has a documented blocker with specific evidence (see Evidence Requirements)
PR Model
Bulk trivial PR
- Branch:
deps/auto-update - Title:
deps: bulk dependency updates (YYYY-MM-DD) - Contains: all updates that build + test cleanly without code changes
- Links to: all non-trivial PRs in the description
Non-trivial PRs
- Branch:
deps/<package-slug>(e.g.,deps/reqwest-0.13,deps/rules-js-v3) - Title:
deps: migrate <package> to <version> - Contains: version bump + all required code changes for that migration
- Description: migration summary, API changes, changelog highlights
Lifecycle
You manage the full set of PRs. On each run:
- List all open
deps/*PRs - Reconcile against current Renovate dashboard
- Open, close, force-push, or update PRs as needed
State Passing Between Runs
You are stateless. Your state lives in the PRs:
- Bulk PR description: tables of what was updated, what's blocked, links to non-trivial PRs. Your future instance reads this first.
- Non-trivial PR descriptions: migration details, what was tried, what worked.
- Commit history: shows what changes were applied.
On every run, start by reading ALL existing dep-update PR descriptions to understand what the previous run already tried and decided. Then diff against the current Renovate dashboard to find what's new or changed.
Fork & Branch Setup
You run as agentydragon-agent without collaborator access. You work on a fork.
- Fork:
agentydragon-agent/ducktape - Upstream:
agentydragon/ducktape - PRs: cross-fork PRs targeting
agentydragon/ducktapebranchdevel
# Ensure remotes
git remote get-url upstream 2>/dev/null || git remote add upstream https://github.com/agentydragon/ducktape.git
git remote get-url fork 2>/dev/null || git remote add fork https://github.com/agentydragon-agent/ducktape.git
git fetch upstream devel
# List all existing dep-update PRs
gh pr list --repo agentydragon/ducktape --author agentydragon-agent \
--search "deps:" --state open --json number,url,headRefName,title
# Check out bulk branch (create or rebase)
git fetch fork deps/auto-update 2>/dev/null && \
git checkout deps/auto-update && git rebase upstream/devel || \
git checkout -b deps/auto-update upstream/devel
Gather Available Updates
From Renovate dashboard
gh issue list --repo agentydragon/ducktape --search "Dependency Dashboard" \
--json number -q '.[0].number' \
| xargs -I{} gh issue view {} --repo agentydragon/ducktape --json body -q '.body'
Parse the "Pending Approval" section for available updates.
Beyond Renovate
Also check for updates Renovate doesn't track:
tf.download(mirror = {...})provider version pins inMODULE.bazeltfdoc_version,tflint_version, OpenTofuversioninMODULE.bazel- Anything else you notice is outdated
Diff against previous state
Compare available updates against existing PR descriptions:
- New updates: attempt to apply
- Previously applied: verify still present after rebase
- Previously blocked: re-check — has a new release resolved the issue?
- Previously skipped as complex: don't retry unless something changed
Evidence Requirements
You MUST actually try every update before classifying it. Run bazel build //... and bazel test //... with the update applied. Only AFTER seeing the
result can you classify the update.
DO NOT
- Use version numbering as a proxy for "breaking" (e.g., "0.x → 0.y is semver breaking" without building). Many 0.x bumps are drop-in compatible.
- Reference TODO.md entries as blocking evidence unless the TODO explicitly says "do not upgrade this dependency".
- Claim "N call sites affected" without listing the actual files and line numbers.
- Declare an update "blocked" or "too complex" without stating specific scope.
- Write vague reasons like "API breaking changes" — name the specific APIs.
DO
- Quote exact error messages with file path and line number.
- Include BuildBuddy invocation links for failed builds/tests.
- For API changes: quote old and new signatures, list every affected call site
as
file.rs:42. - For major version bumps: read the migration guide. State what specifically would need to change, whether there is a replacement API, and your estimate of effort (e.g., "4 files, ~30 lines, mechanical rename" vs "requires rewriting the WebSocket connection setup across 3 modules").
- For "complex migration": give a concrete scope estimate and open a non-trivial PR if the migration is feasible, even if it's a larger diff.
Examples of BAD vs GOOD blockers
Bad: protobuf 34.0.bcr.1 → 34.1 | UPB GCC warnings (tracked in TODO.md)
— The TODO is about pre-existing warnings on the CURRENT version. This does not
block upgrading. You must actually try the upgrade and report what happens.
Good: protobuf 34.0.bcr.1 → 34.1 | bazel build //... fails: external/protobuf+/upb/wire/decode.c:281 -Werror=maybe-uninitialized (BuildBuddy: <link>). Upstream tracking: protocolbuffers/protobuf#17052
Bad: reqwest ^0.12 → ^0.13 | API breaking changes, 12 call sites
Good: reqwest ^0.12 → ^0.13 | Client::new() now returns Result instead of panicking. 4 call sites: finance/worthy/main.rs:365, finance/worthy/converter/fixer_converter.rs:45, finance/worthy/ibflex.rs:280,300. Migration PR: #985
Bad: aspect_rules_js 2.9.2 → 3.0.3 | Major v3 breaking API: pnpm_lock_import removed
Good: aspect_rules_js 2.9.2 → 3.0.3 | v3 removes pnpm_lock_import in favor of npm_translate_lock (migration guide: <link>). Our codebase already uses npm_translate_lock (MODULE.bazel:285,358), so pnpm_lock_import removal does not affect us. Upgrade applied cleanly — included in bulk PR.
Changelog Research
For EVERY dependency version change, read the changelog, release notes, or commit history between old and new version. Document anything the reviewer should know:
- New features relevant to our code (could we use a new API?)
- Bug fixes for issues we've hit or workarounds we've applied
- Deprecations of APIs we currently use
- Behavioral changes (stricter validation, changed defaults)
- New lint rules/checks from linter bumps
The reviewer should understand the semantic content of every update without reading changelogs themselves. Put highlights in the PR description "Changelog Highlights" section and in commit messages.
Good commit message examples:
- "bump ruff 0.8→0.9: adds RUF060 (mutable-default-in-dataclass), 3 new findings in our code — suggest enabling in a followup"
- "bump rules_oci 2.2.7→2.3.0: new
reproducibleattr onoci_image, no action needed" - "bump sqlalchemy 2.0.44→2.0.48: fixes asyncpg connection pool leak under high concurrency (we may have hit this in props)"
Apply Updates
Cross-ecosystem version compatibility
Some dependencies are declared in multiple ecosystems (pip, Bazel, Nix) and their versions must remain compatible. When updating any of these, verify all declarations.
Protobuf is the primary example:
| Ecosystem | Declaration | Example |
| --------- | ---------------------------------------------------------------- | -------------------------------- |
| Bazel | MODULE.bazel: bazel_dep(name = "protobuf", version = "33.1") | protoc gencode 6.33.1 |
| pip | pyproject.toml: protobuf==6.33.1 | Python runtime |
| Nix | nix/packages/default.nix: protobuf (tracks nixpkgs) | Runtime in devShell/home-manager |
Rule: protobuf runtime (pip/Nix) must be >= gencode (Bazel protoc).
Bazel module version X.Y maps to gencode 6.X.Y. If bumping Bazel protobuf,
also bump pip. If Nix lags behind, do not bump Bazel/pip past the Nix version.
When proposing ANY protobuf update, verify all three are compatible and note the versions in the PR description.
Lockfile regeneration by ecosystem
After editing version pins, regenerate lockfiles:
- Python (
pyproject.toml):bazel run //:requirements.update - Rust (
Cargo.toml):CARGO_BAZEL_REPIN=1 bazel build @crates//:all - JavaScript (
package.json): run any Bazel build — pnpm lockfile updates on first build (which fails), then run again - Bazel modules (
MODULE.bazelbazel_dep): no lockfile regen needed - OCI images (
MODULE.bazeloci.pull): update bothtaganddigest. Get new digest:crane digest <image>:<tag>
Batching strategy
Do NOT apply all updates at once and then run a single build. If many things break simultaneously, it's nearly impossible to isolate which update caused which failure.
Work incrementally: apply a small batch, build and test, commit what works, then move to the next batch. How you batch is up to you — by ecosystem, by risk level, or one-at-a-time for risky updates. The key rule: never let the tree get into a state where you have dozens of untested changes stacked up.
If you're resuming an existing PR that already has passing updates, start from that known-good state and add new updates incrementally on top.
Testing
Run bazel build //... && bazel test //... to verify. If something breaks:
- Read the error carefully
- If fixable with a small code change: fix it and include in the bulk PR
- If it requires significant migration: create a non-trivial PR for it
- If not feasible now: revert that update, document why with evidence
Snapshot tests
If snapshot tests fail due to intentional output changes:
bazel test //path/to:snapshot_test \
--test_arg=--snapshot-update \
--remote_executor="" \
--nocache_test_results
Commit the updated .ambr files.
Commit & Push
Make clean, descriptive commits with changelog highlights (see examples above).
Commit messages should include anything the maintainer should know about the update: new features relevant to our code, deprecations, behavioral changes, suggested followups. The maintainer should be able to review the PR by reading commit messages without having to look up changelogs.
# Bulk PR
git push fork deps/auto-update --force
# Non-trivial PRs
git push fork deps/<package-slug> --force
Create or Update PRs
# Bulk PR
gh pr create --repo agentydragon/ducktape \
--head agentydragon-agent:deps/auto-update --base devel \
--title "deps: bulk dependency updates ($(date +%Y-%m-%d))" \
--body "$(cat <<'PREOF'
<bulk PR body — see format below>
PREOF
)"
# Non-trivial PR
gh pr create --repo agentydragon/ducktape \
--head agentydragon-agent:deps/<slug> --base devel \
--title "deps: migrate <package> to <version>" \
--body "$(cat <<'PREOF'
<non-trivial PR body — see format below>
PREOF
)"
To update an existing PR description:
gh pr edit <NUMBER> --repo agentydragon/ducktape --body "$(cat <<'PREOF'
<updated body>
PREOF
)"
Verifying Tests
Follow the standard "Before Hand-off" instructions in AGENTS.md. If a test
failure is clearly pre-existing (also failing on devel), document it in the
PR description but do not let it block you.
Bulk PR Description Format
## Summary
**X** dependencies updated, **Y** blocked (with evidence below),
**Z** non-trivial migrations in separate PRs.
### Updates Applied
| Package | Old | New | Changelog Notes |
| ----------- | ------ | ------ | -------------------------------------- |
| `pydantic` | 2.12.0 | 2.12.5 | Fixes model_copy edge case we may hit |
| `rules_oci` | 2.2.7 | 2.3.0 | New `reproducible` attr on `oci_image` |
| `ruff` | 0.8.0 | 0.9.0 | Adds RUF060, 3 new findings — followup |
### Non-Trivial Migration PRs
- #985 — `reqwest` 0.12→0.13 (`Client::new()` returns Result)
- #986 — `aspect_rules_js` v2→v3 (npm_translate_lock migration)
### Blocked Updates
| Package | Current | Available | Evidence |
| ---------- | ---------- | --------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `protobuf` | 34.0.bcr.1 | 34.1 | `bazel build` fails: `decode.c:281 -Werror=maybe-uninitialized`. Upstream: protocolbuffers/protobuf#17052. [BuildBuddy](link) |
### Changelog Highlights
- **`ruff` 0.8→0.9**: adds RUF060 (mutable-default-in-dataclass), 3 new
findings — suggest enabling in a followup
- **`sqlalchemy` 2.0.44→2.0.48**: fixes asyncpg connection pool leak
### Suggested Followups
TODOs added by this PR (grep for them in the diff):
- `x/agent_server/TODO.md`: drop `async_depends` wrapper after fastapi 0.116
- `TODO.md`: evaluate new ruff rules from 0.9
### Not Tracked by Renovate
| Dependency | Current | Latest | Status |
| ---------- | ------- | ------ | ------------------------------------ |
| `opentofu` | 1.11.2 | 1.12.0 | Not applied: may change state format |
---
<details><summary>Agent state (for next run)</summary>
Last run: YYYY-MM-DD
Bulk branch: deps/auto-update
Non-trivial branches: deps/reqwest-0.13, deps/rules-js-v3
All applied: [list with versions]
All blocked: [list with evidence summaries]
All non-trivial PRs: [list with PR numbers]
</details>
Non-Trivial PR Description Format
## Migration: `<package>` <old> → <new>
### What changed upstream
<1-3 sentences about the key changes from the changelog/migration guide>
### Code changes in this PR
<Summary of what was changed in our code and why>
### Affected files
- `path/to/file.rs:42` — changed `Client::new()` to `Client::new()?`
- ...
### Verification
- Tests: passing on RBE — [BuildBuddy invocation](link)
- Related bulk PR: #980
### Changelog excerpt
<Relevant excerpt from upstream changelog>
Completion Checklist
Before declaring done, verify ALL of the following:
- [ ] Every version change has changelog research documented in the PR
- [ ] Every "blocked" update has specific evidence: BuildBuddy invocation link,
exact error text with
file:line, or changelog citation with API signature diff - [ ] No update is blocked solely by version number pattern (0.x semver) without an actual build/test attempt
- [ ] No update is blocked by a TODO.md reference unless the TODO explicitly says "do not upgrade"
- [ ]
bazel test //...passes on RBE for the bulk trivial PR (BuildBuddy link in PR) - [ ]
bazel test //...passes on RBE for every non-trivial PR (BuildBuddy link in PR) - [ ] Package names are in backticks in all PR description tables
- [ ] Each non-trivial PR has its own description with migration notes
- [ ] The bulk PR links to all non-trivial PRs
- [ ] The union of all PRs + documented blockers covers every Renovate dashboard item
- [ ] Commit messages include changelog highlights for non-patch updates