TL;DR - Most organisations solved dependency updates by switching on a bot, and the bot now opens more pull requests than anyone will ever review. The problem is not the bot. It is that staying current was shipped as a notification to every repository instead of as a capability of the platform, so every team is asked to make the same judgement about the same bump, with less context than anyone else in the building, for a benefit that lands somewhere else. Drift compounds: one version behind is a pull request, five versions behind is a project, and a forced migration is just drift that finally met a date. The fix is to stop treating currency as a per-repo chore. Upgrade the layers the platform ships once, for everyone. Group application updates into one scheduled change per repository and let it merge itself when the fleet’s evidence says it is safe. Run major versions as campaigns with a codemod. And measure lag and version skew, not pull requests opened.
I build and run platforms for a living, and one of the first things I look at in a new engagement is not the architecture diagram. It is the list of open pull requests authored by a bot. In almost every organisation that list is long, old and ignored. A typical repository has thirty to a hundred of them, the oldest from the previous calendar year, several conflicting with each other, a few failing for reasons nobody has looked into. Somebody, at some point, muted the notifications.
That list is a very honest document. It tells you the organisation knows it should stay current, has bought the tooling to do it, and has quietly decided the job belongs to nobody.
When I wrote about the four things that eat a platform team’s second year, the first one was the drift treadmill, and I described it as a cost and then moved on. When I wrote about the migration with a date on it, I said a converged, versioned estate turns a runtime end of life into “a fortnight of the ordinary drift treadmill”. Both posts leaned on the treadmill and neither one said how to run it. This post is that part.
The tell
Pick any five repositories that matter and answer three questions about them:
- How many open update pull requests are there, and how old is the oldest? If the answer is “dozens” and “months”, the bot is generating work faster than the organisation is willing to do it.
- What happens when one goes green? If the answer is “it waits for a human who is busy with something else”, green is not carrying any information, and the team knows it.
- How many distinct versions of your base image, or your main framework, are running in production right now? If nobody can answer without a week of digging, you have not got a currency problem yet. You have an inventory problem in front of it.
If the bot’s queue is long, green means nothing, and the skew question needs a project, the organisation is not staying current. It is accumulating a debt and paying a small amount of notification noise for the privilege of not looking at it.
Why the bot does not fix it
The bot is good software. It is doing exactly what it was configured to do. The failure is in what it was asked to do, which is to take a fleet-wide concern and fan it out as a separate decision in every repository.
Look at what each team is actually being asked:
- The same judgement, made a hundred times. A minor bump of a logging library lands in every service that uses it. Each team has to decide independently whether it is safe, with nothing more than its own test suite to go on. Nobody learns from anybody else’s merge.
- By the people with the least context. The team that owns a checkout service knows checkout. It does not know whether version 4.12 of the HTTP client changed its retry defaults. Somebody in the organisation does, once, and the bot’s design guarantees that knowledge never travels.
- For a benefit that lands somewhere else. Merging an update costs the team time now. The benefit arrives later, and mostly to someone else: the security team that does not have to chase a CVE, the platform team that does not have to run a migration. It is the same arithmetic I described for why good roads get routed around: cost now and theirs, benefit later and yours. That arithmetic loses every time it meets a sprint.
- At a volume that trains people to ignore it. Forty pull requests a week per repository is not a stream of decisions. It is noise, and people respond to noise the way they always do. They mute it.
Put those together and the bot has done something subtle. It has turned an invisible debt into a visible one without changing who pays it. The queue is now a permanent reminder that the work is not getting done, which is arguably worse for morale than not knowing.
This is the same shape as a control shipped without a delivery mechanism. I argued that a security rule without a paved road is a tax developers evade. An update bot without a merge policy, a confidence signal and an owner is exactly that: a rule (“stay current”) delivered as a stream of obligations, with no road that makes satisfying it cheap.
Drift compounds
The reason this matters more than it looks is that the cost of an upgrade is not linear in how far behind you are.
One version behind, an update is a pull request. The changelog is short, the diff is small, and if something breaks, you know which change did it. Five versions behind, you are reading five changelogs, two of which contain deprecations whose removal landed in the third. The test failures interact. Nobody remembers why the pin was added. Now it is a ticket, then an epic, then a thing a team estimates in sprints and keeps deferring because it delivers nothing they can demo.
Keep going and the drift eventually meets a date. A runtime goes end of life, a CVE lands in a version so old the fix was never backported, a cloud provider stops supporting the API your SDK calls. At that point it is no longer drift. It is a forced migration, with a governance problem attached and a deadline you do not control.
I find it useful to say this out loud internally, because it reframes the whole conversation: every forced migration is a few years of unmerged update pull requests, presented all at once with an invoice. The organisations that get through runtime end of life in a fortnight are not better at migrations. They just never let the gap open.
Split the estate by who should own currency
The useful move is to stop asking “who merges updates?” as one question. The estate has three layers, and each one wants a different owner.
- The layers the platform ships. Base images, the shared CI workflows, the infrastructure modules, the template’s build configuration, the internal libraries for logging, auth and telemetry. These are the platform’s product, and keeping them current is the platform’s job, not a hundred teams’ jobs. If upgrading the base image requires a pull request in every repository, the design is wrong, and it was wrong at creation time. This is the payoff for thin templates over fat shared primitives: when the repository references the primitive rather than containing a copy of it, you upgrade the primitive once and the fleet picks it up on its next build.
- The substrate. The orchestrator version, the database engine, the runtime itself. Usually a platform concern too, but upgraded on a cadence with a canary, because a bad substrate upgrade fans out to everyone at once. Ship these changes the way you would ship a production deploy: versioned, staged across a friendly cohort first, with a rollback you have actually tested.
- What each service pulls in directly. The application’s own dependencies. These genuinely belong to the owning team, because the team owns the behaviour. But the platform owns the road that makes keeping them current cheap, and that road is the rest of this post.
In most estates I look at, the first layer is the biggest share of the bot’s noise and the easiest to remove. A bot pull request that bumps the base image tag in eighty Dockerfiles is a design defect expressed as eighty chores. Fix the design and the chores disappear.
Build the road for application dependencies
For the dependencies that genuinely belong to teams, the goal is not more pull requests. It is fewer, better-evidenced decisions. What I would put on the road:
- One grouped change per repository, on a schedule. All patch and minor updates batched into a single pull request, opened at the same time every week. One thing to look at, at a predictable moment, rather than forty interruptions scattered across the week. Teams can reason about one weekly change. Nobody can reason about forty.
- Auto-merge on green for patch and minor. If the pipeline passes, it merges, without a human. This is the single largest lever, and it is also the one people are most nervous about, for a good reason I will come back to.
- Fleet evidence instead of local guesswork. Once the same bump has merged green in thirty repositories and run in production for a few days without a rollback, the thirty-first repository should not have to rediscover that. Publish the fleet’s outcome per version, and let it drive the merge policy: proven bumps auto-merge, unproven ones wait for the early cohort.
- A minimum release age. Do not take a package version the hour it is published. A few days of waiting costs you almost nothing in currency and buys a meaningful amount of protection against a compromised or broken release, which is now a realistic supply-chain risk rather than a theoretical one.
- Major versions are not bot work. A major bump with breaking changes should never arrive as a bot pull request in a hundred repositories. It should arrive as a campaign: the platform team reads the changelog once, writes a codemod, migrates three friendly services end to end, and then opens the pull requests itself. That is the same conversion a forced migration needs, sixty bespoke upgrades turned into one upgrade applied sixty times, just run before anyone set a deadline.
None of this needs exotic tooling. Every mainstream update bot already supports grouping, schedules, auto-merge rules and release-age delays. What is missing in most organisations is not a feature. It is a default config owned by the platform, generated into every repository by the road, and kept current like any other part of it.
Green has to mean something
Here is the honest catch. Auto-merge on green is only safe if green means the service works. In a lot of estates it does not. The pipeline compiles the code, runs a handful of unit tests that mock everything interesting, and declares success.
So when a team pushes back on auto-merge, I take it seriously, and I treat it as the most useful audit of the test suite they will ever get. The update stream is a free, continuous, low-stakes experiment: a stream of small changes the team did not write, arriving every week. If the team does not trust their pipeline to judge a patch release of a JSON library, they should not trust it to judge their own changes either. The bot did not create that problem. It found it.
What I would do with a team in that position:
- Turn on auto-merge for the boring categories first. Dev dependencies, linters, type stubs, test tooling. Low blast radius, and it builds confidence that the mechanism works.
- Add one smoke test that exercises a real path. Start the service, hit the health endpoint and one representative request, against real dependencies where you can. It catches a surprising share of the breakage that matters, and an environment nobody has tested honestly will tell you what you want to hear otherwise.
- Watch the rollback rate. If auto-merged updates are causing incidents, the answer is a better gate, not a human reviewer who will skim the diff and click approve anyway. A human approving a lockfile change is not a safety control. It is a ritual.
Measure lag, not activity
The obvious metric is the wrong one. “Update pull requests merged this month” rewards volume, and it is easy to drive up without making the fleet any more current. It is the dashboard theatre failure in a new costume.
What I would measure instead:
- Lag. How far behind the latest acceptable version each service is, summed across its dependencies. Libyears is a crude but serviceable version of this: the total age gap between what you run and what is current. The trend matters more than the number.
- Skew. How many distinct versions of each platform-shipped layer are running in production. One or two versions of your base image is a converged fleet. Eleven is a fleet you will be migrating by hand next year.
- Time to patched. For the last critical CVE, how long from disclosure to every affected deployable running a fixed version. This is the number security actually cares about, and it is the one that tells you whether the whole system works under pressure.
- Median age of open update changes. If it climbs, the road has a problem, usually a gate nobody trusts or a group that keeps failing for one stubborn dependency.
All four depend on knowing what is running, which is why the inventory comes first. And all four belong in the run budget as a standing line, not as a quarterly “dependency hygiene” initiative. Currency is not a project. It is a property you either maintain continuously or pay for in a lump.
When this is the wrong answer
The off-ramps, because not every estate needs a currency programme.
- Ten services and one team. Book a recurring half day, update everything, done. Grouping rules and fleet evidence are overhead you do not need when the whole fleet fits in one person’s head.
- Regulated change control with validated releases. If every change to production needs a validation record, weekly auto-merge is not available to you. Batch it instead: a quarterly validated dependency release, assembled and tested by the platform, with an expedited path for critical CVEs. Same principle, different cadence.
- The estate is on its way out. A system scheduled for retirement in six months needs security patches and nothing else. Spending effort keeping it current is decorating a building you are about to demolish.
- No tests worth the name. Auto-merge without a meaningful gate is roulette. Fix the gate first, starting with the smoke test, and in the meantime group updates and review them weekly rather than pretending the pipeline is a control.
- You have no platform layers yet. If every repository was hand-built and contains its own copy of everything, the first move is not an update policy. It is building the first road so that there is a shared layer to upgrade once.
One guardrail for the other direction, because currency attracts zealots. “Always on the latest version” is not the goal. Chasing every release the day it lands is churn that buys nothing, and it is a supply-chain risk in its own right. The goal is a bounded window: nothing older than a stated lag, nothing younger than a stated release age, and majors adopted deliberately once somebody has read the changelog. Currency is a means to cheap upgrades and fast patching, not a virtue to be maximised.
The bottom line
Every organisation already pays for staying current. Most pay for it in the worst possible way: a muted bot queue that grows quietly, a CVE that turns into a two-week investigation, and a forced migration every couple of years that is really just the accumulated drift arriving with a deadline.
So take the boring version. Split the estate by who should own currency. Upgrade what the platform ships once, for everyone, by referencing primitives rather than copying them. For what teams own, ship a road: one grouped change on a schedule, auto-merge on a gate that means something, fleet evidence instead of a hundred separate guesses, a release-age delay, and majors run as campaigns with a codemod. Then measure lag, skew and time to patched, and fund it as a standing slice of the run budget. That is the whole premise of platform engineering: the thing every team needs should be done once, well, by the people who own it, rather than a hundred times, badly, by the people who do not.
The question I would ask any platform team: how many distinct versions of your base image are running in production right now, and how old is the oldest one? If you cannot answer, start with the inventory. If you can, and the answer is embarrassing, the next thing to build is not a dashboard. It is the road that makes the oldest version stop existing.
If you are looking at a bot queue nobody merges, or a fleet that turns every runtime end of life into a quarter-long project, that is exactly the kind of problem I help teams work through. Let’s talk.