Untracked is not a gap
Part 1 of this series named a limit: a plan can only speak about dependencies a manager has read. A version in a file no manager knows appears nowhere. It ages silently.
So pinup looks for them. And it has an opinion about what it finds: not every untracked version is a gap.
01 — The opinion in one sentence
What nothing updates belongs on the dashboard, unless the repository says it should stay that way.
With --coverage, pinup searches the checkout after the plan. It looks for two shapes: image references with a path and variables such as HELM_VERSION, each with a three-part version. Whatever no manager read goes into the plan and onto the dependency dashboard under “Not updated by anything”.
A version that deliberately stays put is marked:
services:
legacy:
# pinup: coverage-ignore
image: example/tool:1.4.2
Everything else gets an annotation and so becomes a normal candidate for updates. Which files pinup should not search at all is set in the ignorePaths of the .pinup.yaml or of an existing renovate.json.
02 — The alternative: trust the managers
Renovate updates what its managers find. For everything else you write a regex manager of your own. That is a clear contract: the tool does not guess, and what it reports is right.
That contract is right for a tool that runs in thousands of repositories. A search that guesses produces noise, and noise in thousands of dashboards is worse than a gap. Only: whoever never wrote the regex manager also never learns that it is missing.
03 — The price
A search that has to guess. Without a manager, pinup does not know what a string means. So it only looks for two shapes that proved precise. A pin like name@1.2.3 or a version inside a URL it does not find.
Two-part versions stay out. Tags such as valkey:8.1-alpine or variables such as PHP_VERSION: "8.5" choose a line whose patches they follow anyway. A new line is a decision, not an update. That is right most of the time and sometimes not.
Markers in the code. Whoever pins on purpose has to write it down. That is one more line, but it also tells the next person that the pin is intended.
04 — The occasion
On 25 September 2026 I searched ten repositories of my estate. They held 774 versions. The plan knew 374 of them, 27 stood under an annotation, 6 in lock files, 141 were noise. That left 226 candidates.
173 of them were no gap
173 of the 226 sat in a single repository: my Wolfi packages. None of them was a gap. Most belong to upstream recipes that I preserve but do not build. Their versions age on purpose. Had pinup reported them as gaps, the dashboard would have lied. Hence the title of this part and the markers.
The real gaps
I checked each of the remaining 53 by hand. About 22 were real pins that nothing updated:
- images in Compose files, because no manager read them,
- the images in a
lefthook.ymlcopied from a template into every repository, - tool versions in CI variables without an annotation,
- annotations no manager read because their form did not fit it: a missing
versioning=, a shell-style assignment, single instead of double quotes.
Most of them are fixed by now: with annotations, a manager for lefthook.yml in the runner configuration and one for Compose images.
05 — The enforcement
- Every exception has a test. Comments, prose, tests, excluded paths, markers, two-part tags: for every exception there is a case that must stay quiet. Next to them stand cases that must report.
- Breaks for coverage. Six mutations check, among other things, that the marker works, that the dashboard shows the section, that
--coveragedoes something and thatadvise --initpasses its own checks. - Plan and checkout are rarely the same state. If pinup has just raised a pin, the plan still knows the old value. So a line carrying the name of a known dependency counts as tracked. That came from a false alarm in my own estate.
For new repositories
pinup advise --init proposes a first .pinup.jsonc for a repository without a configuration. If it finds pins no manager reads, it adds an annotation manager for exactly those files, with the annotation for each pin. The proposal has to pass advise's own checks.
Frequently asked questions
How do I switch the search on?+
With --coverage on pinup whatif or pinup run, or with PINUP_COVERAGE=1 in the runner. From an existing plan it works with pinup advise --config .pinup.yaml --plan … --repo .. Targeted runs skip the search: the fast lane after a release of my own, and runs for a single package.
Can I exclude a whole file?+
Yes. pinup: coverage-ignore-file anywhere in the file excludes it completely. For whole directories, the configuration's ignorePaths apply.
Why doesn't pinup write the annotation itself?+
Because the decision belongs to the repository. Whether a pin is updated, read differently or left on purpose, pinup cannot know. It proposes, for instance with advise --init, and a person decides.
Conclusion
What no manager reads ages silently. pinup makes it visible, where the owners read. But it distinguishes: a version that stays on purpose is not a gap, and a repository may say so.
Next week: the line between deciding and carrying out. The core decides, plugins apply. The overview of all parts and the terms that recur are in the opening of the series.
Which versions in your repositories does nobody update? I will find them.
A coverage scan of your repositories with pinup: which pins no manager reads, which annotations point at nothing and which pins may stay put on purpose.
About the author

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.
