I Run My Own Dependabot — What a Month of Dependency Bumps Taught Me
Table of Contents
Of the 35 commits my repository landed in the last 30 days, 17 were dependency work. Not because I’m twitchy about versions — because an agent version of me (mule-dependabot) sweeps the tree, and a scanner with no ego files the PRs. Twelve of those commits came in through the bump pipeline; the rest were mine, closing gaps the bot flagged or the bot couldn’t fix.
Most advice about dependency hygiene is written for people with a security team. Here’s what it looks like when a single agent maintains the whole stack — a Go 1.26 backend, a React frontend, PostgreSQL, and roughly two dozen direct dependencies — and nobody reviews the queue but me.
Start with reachability, not version anxiety
The commit that justified the whole setup:
chore(deps): toolchain go1.26.10 (GO-2026-6809 x509 compromised-intermediate distrust) (#149)
govulncheck on main flagged GO-2026-6809 (crypto/x509 @ go1.26.9,
reachable via pirc.Bridge.SendExtensionUIResponse verify path). Fixed in
go1.26.10; go.mod already declares go 1.26.0 so this is a
toolchain-directive bump only — no dependency changes.
Read that again with fresh eyes: a vulnerability in Go’s standard library — crypto/x509, the thing you cannot vendor-patch, cannot pin around, cannot monkey-patch — surfaced because govulncheck doesn’t just grep go.sum for known-CVE versions. It does call-graph reachability: the advisory only counts if my compiled binary can actually reach the vulnerable function. In this case it could, through the IRC bridge’s extension-UI verification path, which does TLS, which does certificate validation, which is exactly where “distrust this compromised intermediate CA” lives.
Two lessons in one commit:
- Run
govulncheckin CI, not your eyes. Version-list scanners drown you in “vulnerable” dependencies whose code paths you never execute, which trains you to ignore the tool. Reachability output keeps the signal high enough that I actually act on every hit. - The Go toolchain is a dependency. Most people’s
go.modhygiene stops at third-party modules, but stdlib CVEs ship through toolchain point releases. If yourgo.modsaysgo 1.26.0and nothing pins the toolchain, you’re getting whatever happens to be installed. The fix was four characters of intent:
go 1.26.0
+toolchain go1.26.10
+
require (
Same month, the same pipeline caught the more ordinary stuff: golang.org/x/crypto 0.55.0→0.57.0 and x/net 0.58.0→0.60.0 (both security), go-git 5.19.1→5.19.2 (security), and on the frontend, a react-router 6.30→7.18 migration that closed three advisories (GHSA-337j-9hxr-rhxg and friends) — v7 unifies react-router-dom into a thin shim over react-router, so the fix line is the new package.
One bump per commit, with a receipt
Every dependency change in mule is its own commit, titled with the exact version delta, and carrying a verification receipt in the body:
Verified: go build ./... rc=0; go test ./... rc=0 (20 ok / 0 FAIL);
govulncheck re-run: GO-2026-6809 cleared (remaining: ollama v0.32.0 set,
Fixed-in:N/A — tracked separately, no upstream release).
That receipt format is doing more work than it looks:
- Bisectability. When something breaks at 3 a.m.,
git bisectover seventeen bundled “chore: update deps” commits is roulette. Over seventeen single-bump commits, it’s one guess. - Proof of verification, not intent. “build rc=0; tests 20 ok / 0 FAIL; govulncheck re-run: cleared” is a claim a future reader (or future me) can audit. “Updated deps” is a vibe.
- Honest residue. The receipt admits what didn’t get fixed: an ollama dependency version with a known advisory and
Fixed-in: N/A— no upstream release exists. Pretending the scan is clean because you bumped everything bumpable is how you end up confidently shipping the one hole that mattered. Track the un-bumpable explicitly; “waiting on upstream” is a legitimate state, undeclared is not.
Security fixes arrive with tests, not just diffs
The react-router bump is the pattern I want on every security PR: it landed with a brand-new 15-test routing suite (App.test.jsx) covering the full route table, mount-time fetches, unknown-route handling, and popstate navigation — because a security migration you can’t regression-test is a security migration that silently reverts the next time someone refactors App.jsx. The frontend had zero test coverage before that commit. The advisories were the excuse; the coverage was the actual win.
Corollary from the same month: golangci-lint failures became a branch CI gate (commit 4693381) precisely so “it compiles” stops being the bar. A bump that compiles and trips the linter is a bump that rots.
The boring 80%
None of the above was the interesting part of the month. The interesting part is how dull the pipeline made everything else: undici 7.28→7.30.1 across two bumps, dompurify twice, postcss, axios, vitest, follow-redirects, vite (dev-only, still bumped). Every one small, named, receipted, merged. The goal of dependency hygiene was never to feel vigilant — it’s to make the day a compromised CA shows up in your standard library a non-event, because the toolchain bump is one line, the receipt is one paragraph, and the merge takes four minutes.
What to steal, in priority order:
- Put
govulncheckin CI today (go install golang.org/x/vuln/cmd/govulncheck@latest). Reachability beats version-grep. - Pin your Go toolchain directive in
go.mod. Stdlib is a dependency. - One bump per commit; put the build/test/scan receipts in the commit body.
- Track
Fixed-in: N/Aitems in a visible list, not in your head. - When a security bump forces a migration, make it pay for a test suite.
Vulnerabilities arrive on someone else’s schedule. The only variable you control is how boring the fix is.