Why Renovate Never Updated Our Own Packages — A Trace
I run several TYPO3 14 installations, each as its own app repo pulling in a dozen of my own moselwal/* Composer packages plus forked extensions. Every time I released a sitepackage, I had to manually follow up the version bump in the app repo — composer update, commit, merge. Renovate ran reliably for external packages, but never touched my own. The obvious assumption was that a planned fast-lane configuration was still missing. The reality was four layers deeper — and each layer obscured the next.
TL;DR
TL;DR: Four nested causes kept Renovate from ever bumping my first-party moselwal/* packages: a custom.regex manager that reads caret ranges as “already current”; the same regex manager never touching composer.lock; a Renovate container where composer wasn't installed at all under the GitLab docker executor; and, as a bonus, an unfiltered registry entry that routed every external package lookup through a 404 detour to our own GitLab registry. The fix: pin first-party deps exactly, add a postUpgradeTask to refresh the lockfile, install composer explicitly in before_script, and add an only filter to the registry.
What happened
The symptom
Renovate ran reliably for me — for external packages. Internal updates never happened. Every time I released a sitepackage, I had to manually follow up the bump in the app repo: composer update, commit, merge, every single time. The obvious assumption was that the planned fast-lane for my own packages simply hadn't been rolled out yet. The reality sat four layers deeper, each plausible on its own, each obscuring the next.
Cause 1: caret ranges are invisible to the regex manager
My moselwal/* packages live in GitLab's group-wide Composer registry. Its endpoint serves versions at the p2/ path as an object-keyed dict instead of an array — Renovate's packagist datasource doesn't parse that. The original author worked around it: a custom.regex manager reads versions directly from git tags.
Clever — but with a catch. A regex manager doesn't read composer.lock. It only sees "moselwal/wehning-site": "^1.0" in composer.json. And for a caret range, the “current version” is by definition the highest matching one — i.e. 1.8.0, the latest. There's nothing to bump. updates: [], every time, regardless of rangeStrategy.
An isolated renovate --dry-run confirmed it in black and white: currentVersion: 1.8.0, updates: [].
# Before — invisible to a regex manager
"moselwal/wehning-site": "^1.0"
# After — pinned exactly, regex manager sees 1.6.0 < 1.8.0
"moselwal/wehning-site": "1.6.0"
Fix: pin first-party deps exactly ("1.6.0" instead of "^1.0"). Now the regex manager sees 1.6.0 < 1.8.0 and produces an update.
An alternative for other setups: if you don't want to give up the caret range, you can instead give the regex manager a currentValueTemplate that extracts the actually-installed version from composer.lock instead of taking the raw range from composer.json. For me, exact pinning was the simpler path — both approaches solve the same underlying problem: the regex manager needs a real, concrete “current version” to compare against, not a range.
Cause 2: the regex manager doesn't write the lockfile
Exact pin set — and Renovate did open a branch bumping composer.json from 1.6.0 to 1.8.0. But composer.lock stayed untouched. Content-hash mismatch → composer install fails → branch pipeline red → no auto-merge.
The reason: Renovate only calls composer updateArtifacts (which refreshes the lockfile) for the composer manager, not for custom.regex. The bump came from the regex manager → the lockfile was left behind.
postUpgradeTasks:
commands:
- "composer update {{{depName}}} --no-install --ignore-platform-reqs"
fileFilters:
- composer.lock
executionMode: branch
Fix: a postUpgradeTask on the first-party rule: composer update <dep> --no-install …, plus whitelisting the command in RENOVATE_ALLOWED_COMMANDS.
Cause 3: composer doesn't even exist in the Renovate container
The postUpgradeTask ran — and failed with spawn composer ENOENT. Composer wasn't installed. Renovate normally installs tools on demand via containerbase. Except: under the GitLab docker executor, which overrides the image entrypoint, that on-demand install never triggers. No Installing tool log, nothing — Renovate tried to launch composer straight from PATH, where it didn't exist. RENOVATE_BINARY_SOURCE=install didn't change that either.
This was the real hammer: it explains why Renovate had never refreshed a composer lockfile on any of my repos — not my own packages, and not the external ones either.
before_script:
- install-tool php
- install-tool composer
Fix: install the tools explicitly in before_script. containerbase pulled PHP 8.5.8 and Composer 2.10.2 in about 3 seconds. ENOENT disappeared.
Cause 4 (the bonus): the 404 storm
Along the way I stumbled onto the second root cause of the slowness. My composer.json listed the GitLab registry without a filter — so composer queried it for every package, including the hundreds of external ones. Each one meant a 404 round-trip to the (RAM-starved) GitLab box before composer fell back to Packagist.
{
"repositories": [
{
"type": "composer",
"url": "https://gitlab.example.com/api/v4/group/123/-/packages/composer/packages.json",
"only": [
"moselwal/*",
"netresearch/nr-passkeys-be",
"sgalinski/*"
]
}
]
}
Fix: an only filter on the registry — composer now only queries it for first-party packages and forks. The catch: forked external packages (netresearch/nr-passkeys-be, sgalinski/*) keep their upstream name and must be listed in only, or composer silently fetches the upstream instead of the fork. Over-inclusion is harmless (404 → fallback); under-inclusion breaks forks.
I hit a counterexample along the way too: a commit tool that mangled JSON newlines took down my Renovate runner config twice. Robust commits beat convenient ones.
The lesson
What carried me through four nested causes wasn't guessing, but isolated
renovate --dry-runruns and consistently reading the job logs — proving each hypothesis individually before touching the shared config. A custom regex manager is a workaround with its own blind spots: it only reads what you tell it to read — never the lockfile, never the full bump cycle a native manager silently brings along.
Frequently asked questions
Is too broad an only filter on the registry dangerous?+
Over-inclusion is harmless — composer just gets a 404 and falls back to Packagist. Under-inclusion is the dangerous one: forked packages keep their upstream name, and without an entry in only, composer silently fetches the upstream instead of your own fork — the exact opposite of harmless.
Why doesn't Renovate just report that composer is missing?+
Because the postUpgradeTask simply fails with spawn composer ENOENT, and containerbase's on-demand install never triggers under the GitLab docker executor, which overrides the image entrypoint. No Installing tool log, no hint — just a silent branch failure.
Why isn't it enough to just set rangeStrategy to pin?+
Because the problem isn't the rangeStrategy — it's that a regex manager has no concept of “current vs. desired version” beyond the caret range itself. With ^1.0, 1.8.0 is formally “current” no matter which strategy is set — the range has to be pinned exactly before the manager can see any difference at all.
Conclusion
Four causes, each plausible on its own, each obscuring the next: a regex manager that reads caret ranges as already current; the same manager never writing a lockfile; a missing composer binary in the Renovate container; and an unfiltered registry entry with fork risk. In the end it came down to five key lines in the shared Renovate config and two small changes per consumer repo — and my own packages now flow through just as automatically as the external ones.
I find the four layers silently breaking your Renovate config — before you have to manually chase the next bump yourself.
Isolated renovate --dry-run diagnosis against your actual config, auditing custom.regex managers for blind spots around lockfiles and caret ranges, and a look inside the Renovate container itself — before the next release bump ends up manual again.
Platform operations, not consulting on paper: I build and continuously run your CI/CD and dependency-update pipelines.
About the author
![[Translate to English:] Foto von Kai Ole Hartwig.](/fileadmin/_processed_/e/9/csm_ole-neu_73323ad80d.jpeg)
Kai Ole Hartwig
Programming since 2002 – self-taught, set up my own business with KO-Web in 2012. Over 100 projects, with a focus on security, performance, automation and quality. Today freelance: DevSecOps consulting, training and software development.