dependabot / dependabot/dependabot-core
docker latest_tag walks all canonical tags when the registry uses v-prefixed releases
- Dominant language
- Ruby
- Stars
- 5.8k
- Forks
- 1.5k
- Avg merge
- 2d 18h
- Merged PRs (30d)
- 149
Description
### Is there an existing issue for this?
- [x] I have searched the existing issues
### Feature description
For Docker images whose recent releases use a v prefix (e.g. `grafana/grafana-image-renderer:v5.8.3`), Dependabot's `latest_tag` method fails to recognize them as canonical. Combined with the registry's latest floating tag pointing at a v-prefixed release, this causes Dependabot to issue a manifest HEAD for every older non-prefixed canonical tag in the repository before giving up — ~400 wasted requests for grafana/grafana-image-renderer on every update check, each preceded by a 401/auth round trip.
There is also a latent correctness bug: latest_tag is consumed by prerelease?, which treats candidates with a numeric version higher than latest_tag as pre-releases. A stale or incorrect latest_tag can silently reject valid updates.
# Reproduction
grafana/grafana-image-renderer on Docker Hub:
- Older tags: 1.0.1, 2.0.0, ..., 3.12.9 (no v prefix)
- Newer tags: v4.x, ..., v5.8.3 (with v prefix)
- latest → resolves to v5.8.3's digest
When updating from v5.8.3, the proxy log shows:
```
HEAD .../manifests/latest → 200
HEAD .../manifests/3.12.9 → 200
HEAD .../manifests/3.12.8 → 200
... (every tag down to 1.0.1) ...
INFO Latest version is v5.8.3
```
# Root cause
Tag#canonical? in docker/lib/dependabot/docker/tag.rb:131:
```ruby
def canonical?
return false unless numeric_version
return true if name == numeric_version
return true if numeric_version && name == numeric_version.to_s + "-sdk"
numeric_version && name == "jdk-" + T.must(numeric_version)
end
```
numeric_version strips the v because VERSION_REGEX at tag.rb:14 places v? outside the named capture:
`VERSION_REGEX = /v?(?[0-9]+(?:[_.][0-9]+)*...)/i`
So for v5.8.3: numeric_version == "5.8.3", but name == "v5.8.3" — name == numeric_version is false, and canonical? returns false.
Then in update_checker.rb:567:
```ruby
def latest_tag
return unless latest_digest
tags_from_registry
.select(&:canonical?)
.sort_by { |t| comparable_version_from(t) }
.reverse
.find { |t| digest_of(t.name) == latest_digest }
end
```
With every v-prefixed tag filtered out, the canonical list maxes out at 3.12.9. None of those tags' digests match latest_digest (which points at v5.8.3), so find walks the entire list, issuing one digest_of (a manifest HEAD, plus auth) per tag, and eventually returns nil.
Suggested fix from Claude
Accept v-prefixed tags as canonical. One-line addition to canonical? at tag.rb:131:
def canonical?
return false unless numeric_version
return true if name == numeric_version
return true if name == "v#{numeric_version}"
return true if numeric_version && name == numeric_version.to_s + "-sdk"
numeric_version && name == "jdk-" + T.must(numeric_version)
end
This:
- Lets latest_tag find v5.8.3 immediately (the first-canonical-match short circuit kicks in).
- Eliminates ~400 wasted HEAD/auth round trips per check on this image.
- Closes the latent prerelease? bug for any project that uses v-prefixed canonical tags.
A test belongs in docker/spec/dependabot/docker/tag_spec.rb asserting Tag.new("v5.8.3").canonical? is true, plus an update_checker spec where latest resolves to a v-prefixed tag and the walk stops on first hit.
Optional defensive follow-up
Even with the fix, registries can have latest pointing at a non-canonical tag (suffix variants, multi-arch index aliases, etc.), which would still trigger a full walk. As a safety net in latest_tag, short-circuit when the highest canonical tag is older than the current installed version — latest can't usefully resolve below the user's current version, and continuing the walk only burns requests:
canonical = tags_from_registry.select(&:canonical?)
.sort_by { |t| comparable_version_from(t) }.reverse
return if canonical.empty?
current = comparable_version_from(Tag.new(T.must(dependency.version)))
return if comparable_version_from(T.must(canonical.first)) < current
canonical.find { |t| digest_of(t.name) == latest_digest }
That is a separate, smaller PR and can wait — the v-prefix fix alone resolves the reported case.
Contributor guide
Research direction
Start in docker/lib/dependabot/docker/tag.rb at Tag#canonical? and review the existing tag specs in docker/spec/dependabot/docker/tag_spec.rb. Then inspect update_checker.rb:567 and its related specs to verify that a v-prefixed latest tag is recognized without walking older tags. Done means the tag case and latest-tag lookup tests pass with no unnecessary digest requests.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- docker, ruby
- Domain
- tooling
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100