dependabot / dependabot/dependabot-core

docker latest_tag walks all canonical tags when the registry uses v-prefixed releases

Open Beginner friendly
#15,080 0 comments 0 reactions 0 assignees View on GitHub
L: docker L: ruby:bundler T: feature-request
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.