Supply chain

A dependency bot that is not allowed to merge

Renovate runs as a weekly Kubernetes job, inventories pinned dependencies, and opens reviewable pull requests when upstream releases move. Routine updates are grouped, major versions travel alone, and every branch runs the same secret, syntax, and vulnerability gates as an authored change.

The bot cannot merge. A model-based risk classifier can label a pull request and explain its judgment, but the automerge decision remains behind a disabled feature flag and an evaluation suite that has not graduated from observe mode. An unreadable classifier answer becomes high risk.

Built from the Renovate configuration, Kubernetes CronJob, custom pin matchers, CI workflow, risk classifier, guarded automerge decision, and live apply notes in the repository.

The cost of pinning everything

This lab pins aggressively. Container images are referenced by digest rather than tag, so the bytes that ran yesterday are the bytes that run today. Command-line tools used by the build pipelines are pinned to an exact release. Linting hooks, workflow actions and controller plugins all carry explicit versions.

That buys reproducibility, and it buys a specific, well-known problem in exchange. A pin never moves on its own, so the lab drifts steadily further behind upstream, silently, and the gap is invisible until something forces an upgrade. The usual outcome is that nothing gets updated for months and then everything gets updated at once in a single miserable session where nobody can tell which of forty changes broke the build.

The honest alternative to a bot here was a recurring calendar reminder to go and check each pinned surface by hand once a month. That is written down as the fallback, and it is genuinely the better option if the bot proves unreliable, because a monthly sweep that actually happens beats an automated one that everybody has learned to ignore.

How it runs

Renovate is the dependency bot, self-hosted. It runs as a Kubernetes CronJob, once a week, Monday at 03:00. It is not a plugin on the build server, which was a deliberate choice: this lab has a standing rule against adding plugins to the build controller, because every plugin is another thing with access to the build system and another supply chain to trust. A scheduled container that talks to the git server over its API needs none of that.

The bot's own image is pinned by digest as well, which is the sort of thing that is easy to skip on the assumption that the tool doing the pinning does not need pinning. It runs as its own restricted service identity on the git server, scoped to write to exactly one repository and nothing else, with its token read from the lab's secrets server rather than stored on disk.

Renovate weekly discovery, grouped pull request, normal CI review and disabled automerge path
Renovate owns discovery and branch creation. The repository's ordinary checks and reviewer own acceptance. The classifier remains advice while both automerge guards stay closed.

Its configuration is split in two on purpose. The global half (where the git server is, who the bot is, which repositories it looks at) lives with the deployment. The half that decides how updates are grouped and what gets tracked lives in a config file in the repository itself, so changing the update policy is a reviewed pull request against the repo it affects rather than a change to a running cluster object.

What it watches

Some of this is free. Renovate natively understands container base images in Dockerfiles, Python requirements files, Helm charts and their values, the version pins in the pre-commit config, and the pinned actions in the CI workflow files. Those needed no configuration at all.

The interesting part is the surfaces it does not understand, because they are conventions this lab invented rather than a standard file format. There are three, and each is now tracked by a custom matcher paired with a # renovate: comment sitting directly above the pinned line, telling the bot where the version lives and which upstream to check:

The command-line tools the pipelines install (the secret scanner, the vulnerability scanner, the manifest validator, and the signing tool) each pin a release in a shell variable. The image digest pins used to promote a build from one environment to the next, so a digest change arrives as a reviewable diff instead of a silent repoint. And the build controller's own plugin list, including the lines that are commented out awaiting an operator, so a pin does not go stale in the window before it is switched on.

The failure mode for a custom matcher is that it silently matches nothing. A typo in the annotation or the file pattern does not produce an error, it produces a bot that quietly skips that file forever and a repo that looks fully covered. Two offline tests guard against it: one checks the configuration parses, the other checks every annotated pin is still tracked by a matcher. Beyond that, the check is to read the bot's own inventory after a run and confirm each pinned surface actually appears in it.

Two things remain untracked and are written down as such: tool versions passed as build arguments inside container builds, and Helm chart versions pinned inside the upgrade pipelines.

Every update is an ordinary pull request

Non-major updates arrive grouped into a single pull request. Each major version bump gets its own, because a major is where the breakage is and it deserves to be reviewable and revertable on its own.

Those pull requests are not special. They run the same checks as a change written by hand: secret scanning, linting, and vulnerability scanning of the resulting images. This lab has a separate, narrow lane where automated commits skip CI, used only for machine-written status records that nothing is built from. Dependency updates are explicitly excluded from it, and the bot's commit prefix is chosen so it cannot accidentally trip that lane. A dependency update that skipped the gates would be the one change class you least want unchecked.

Observe mode, and the two guards

Reviewing a grouped update every week is still work, and most of that work is deciding that a patch bump to a linting tool is boring. So there is a classifier that reads a proposed update and tags it low, medium or high risk, and a decision module that would let low-risk ones merge themselves once the required checks are green.

That module is committed and it has never merged anything. It decides and it explains its decision, and something else entirely would have to do the merging. Two independent guards keep it off. The first is a plain feature flag, off by default. The second is more interesting: the decision reads the mode of the evaluation suite that scores this kind of automated judgment, and refuses while that suite is still in observe mode rather than acting as a required check. Its committed state is observe, so flipping the feature flag alone changes nothing.

Tying the second guard to the evaluation suite's own configuration rather than to a second switch is the part worth copying. It means automerge cannot go live before the thing that measures whether its judgment is any good has finished measuring, and that ordering is enforced by construction rather than by remembering. The classifier also fails safe: an answer it cannot parse is treated as high risk, never low.

Major version bumps are permanently excluded from automerge.

To be exact about the current state: the bot is running and opening pull requests, and the automerge path has never merged anything and cannot until a deliberate, ordered set of steps is taken. Nothing on this page describes a robot with commit access.

How it runs now

WhenWhat runsWhat happens next
Weekly scheduleThe pinned Renovate container reads its repository configuration and dependency inventory.No update produces no branch. A detected update enters grouping rules.
Routine version changeRenovate groups compatible non-major updates into one pull request.The reviewer sees one bounded maintenance diff.
Major version changeRenovate opens a separate pull request.The change remains isolated for migration work and rollback.
Every bot pull requestThe normal required checks scan secrets, validate configuration, and inspect affected images.A failing gate blocks the same way it would for any other author.
Optional classifier runThe local model labels risk and records reasons.The label helps review. It does not grant merge authority.

What broke while I was building it

The core loop works: it authenticates, detects updates, pushes a branch, and opens the grouped pull request. Two smaller things do not work against this git server, and they are documented rather than worked around, because the pull requests are the product and the rest is convenience.

It cannot create its dependency dashboard, the summary issue listing everything it can see and everything pending. This was initially assumed to be a token permission problem, the bot's access was widened to cover it, and it kept failing, which was useful: it ruled out the obvious cause and pointed at the git server instead, either at the issue tracker being disabled on that repository or at a version older than the bot expects for that part of its API. It does not block anything.

Its pull requests carry no release notes. Renovate normally fetches upstream changelogs, but this self-hosted deployment has no credential for that service, so it cannot pull them. The version change and the diff are still there. It is a cosmetic gap with a known fix that has not been judged worth the extra credential.

If it turns out to be flaky in practice, the plan is not to fight it. Suspend the schedule and fall back to the monthly manual sweep. That fallback being written down before it was needed is the reason adopting the bot was a low-risk decision rather than a bet.

What I would do differently

I would build a coverage report before enabling the schedule. Custom matchers can fail by matching nothing, which looks like a quiet, successful run. The regression tests now check that annotated pins still belong to a matcher.

I would also keep automerge out of the first adoption entirely. The current two-guard design is safe, but it adds conceptual weight to a page whose working result is simpler: the bot remembers the updates and a reviewer decides what lands.