Enhance Docker update checker to handle non-semver tags#14337
Enhance Docker update checker to handle non-semver tags#14337jpinz merged 20 commits intodependabot:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR enhances the Docker update checker to handle non-semver (date-embedded) tags by introducing two changes gated behind the docker_created_timestamp_validation experiment flag:
- A
dated_version?method onTagthat detects 8-digit date components (YYYYMMDD) to prevent cross-updates between dated and non-dated tags. - A
fetch_image_config_createdmethod that reads the Docker image config blob'screatedtimestamp to verify actual image recency when semver comparison is misleading.
Changes:
- Refactors
version_related_pattern?into a class-level constantVERSION_RELATED_PATTERNSwith updated matching logic (removing the old broad prerelease pattern andrc/jremarkers, and adding a digit-leading mixed pattern for PEP 440 prereleases). - Adds timestamp validation via new
validate_tag_with_timestamp,fetch_image_config_created, andresolve_platform_manifestmethods to theUpdateChecker. - Adds
dated_version?andcomparable_to?changes toTagso dated and non-dated tags are treated as incomparable when the experiment is enabled.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
docker/lib/dependabot/docker/tag.rb |
Adds dated_version? method; modifies numeric_version to strip date components and comparable_to? to treat dated/non-dated tags as incomparable when experiment is enabled |
docker/lib/dependabot/docker/update_checker.rb |
Adds MANIFEST_LIST_TYPES and VERSION_RELATED_PATTERNS constants; refactors version_related_pattern?; adds validate_tag_with_timestamp, fetch_image_config_created, fetch_image_config_created_from_registry, resolve_platform_manifest, and candidate_newer_by_created_date? methods; fixes dead select call; improves sort_tags tiebreaker |
docker/spec/dependabot/docker/update_checker_spec.rb |
New tests for golang/node alpine suffix matching, aspnet dated-tag scenarios (with/without experiment), and version_related_pattern? coverage |
docker/spec/dependabot/docker/tag_spec.rb |
New tests for dated_version?, comparable_to?, and numeric_version |
docker/spec/fixtures/docker/registry_tags/aspnet.json |
New fixture for aspnet with dated and non-dated tags |
docker/spec/fixtures/docker/registry_tags/aspnet_with_future_dates.json |
New fixture with a newer same-base-version dated tag |
docker/spec/fixtures/docker/registry_tags/aspnet_with_future_tags.json |
New fixture with 4.8.2 tagged versions for upgrade testing |
docker/spec/fixtures/docker/registry_tags/golang.json |
New fixture for golang versions with alpine suffixes |
docker/spec/fixtures/docker/registry_tags/node_alpine.json |
New fixture for node versions with alpine suffixes |
Comments suppressed due to low confidence (2)
docker/lib/dependabot/docker/update_checker.rb:824
- The
fetch_image_config_created_from_registryandresolve_platform_manifestmethods contain non-trivial logic (manifest list resolution, platform selection preferringamd64, config blob parsing, andTime.parsefor thecreatedfield) but all tests stubfetch_image_config_createdat the higher level. There are no tests exercising the actual registry interaction path for these methods. Adding unit tests forresolve_platform_manifest(e.g., with a manifest list input vs single-platform input) and the fullfetch_image_config_createdpath would improve confidence in the implementation.
sig { params(tag_name: String).returns(T.nilable(Time)) }
def fetch_image_config_created_from_registry(tag_name)
manifest = with_retries(max_attempts: 3, errors: transient_docker_errors) do
docker_registry_client.manifest(docker_repo_name, tag_name)
end
resolved = resolve_platform_manifest(manifest)
return nil unless resolved
config_digest = resolved.dig("config", "digest")
return nil unless config_digest
config_blob = with_retries(max_attempts: 3, errors: transient_docker_errors) do
docker_registry_client.doget("v2/#{docker_repo_name}/blobs/#{config_digest}")
end
config_data = JSON.parse(config_blob.body)
created_str = config_data["created"]
return nil unless created_str
Time.parse(created_str)
end
# Resolves a manifest to a single platform-specific manifest.
# If the manifest is a manifest list (multi-arch), selects the most
# appropriate platform (preferring linux/amd64).
sig { params(manifest: T.untyped).returns(T.nilable(T::Hash[String, T.untyped])) }
def resolve_platform_manifest(manifest)
media_type = manifest["mediaType"] || manifest[:mediaType]
unless MANIFEST_LIST_TYPES.include?(media_type)
# Already a single-platform manifest
return manifest.is_a?(Hash) ? manifest : manifest.to_h
end
manifests = manifest["manifests"] || manifest[:manifests] || []
return nil if manifests.empty?
# Prefer linux/amd64, fall back to first available
selected = manifests.find do |m|
platform = m["platform"] || m[:platform] || {}
(platform["architecture"] || platform[:architecture]) == "amd64"
end
selected ||= manifests.first
# Fetch the platform-specific manifest to get the config digest
platform_digest = selected&.dig("digest") || selected&.dig(:digest)
return nil unless platform_digest
platform_manifest = with_retries(max_attempts: 3, errors: transient_docker_errors) do
docker_registry_client.doget("v2/#{docker_repo_name}/manifests/#{platform_digest}")
end
JSON.parse(platform_manifest.body)
end
docker/lib/dependabot/docker/update_checker.rb:257
- When the experiment is enabled,
validate_tag_with_timestampmay callfetch_image_config_createdfor each candidate tag starting from the highest-ranked one. Each call that isn't cached involves up to 3 registry HTTP requests (manifest fetch, possibly platform manifest fetch, and blob fetch), each with up to 3 retries. For images with many candidate tags that all share the same semver (e.g., many dated variants), this could result in a large number of API calls. Consider adding an explicit limit on how many candidates are checked via timestamp (e.g., only check the top N candidates), or at least logging the number of timestamp lookups performed.
# Walk backwards through candidates to find one actually newer by build date
candidate_tags.reverse_each do |tag|
next if tag.name == current_tag.name
next if comparable_version_from(tag) < comparable_version_from(current_tag)
if candidate_newer_by_created_date?(tag, current_tag)
Dependabot.logger.info(
"Timestamp validation: #{tag.name} confirmed newer than #{current_tag.name}"
)
return tag
end
Dependabot.logger.info(
"Timestamp validation: skipping #{tag.name} — image created date " \
"is not newer than #{current_tag.name}"
)
end
current_tag
end
docker/spec/fixtures/docker/registry_tags/aspnet_with_future_dates.json
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
docker/spec/dependabot/docker/update_checker_spec.rb:1715
- The
describecontext description at line 1715 says "when upgrading from a date-tagged version to another (both have dates in tag)", but the test body assertslatest_version == current_version(no upgrade occurs). The test is actually verifying that a downgrade to an older dated tag is rejected, not that an upgrade happens. The context description is misleading and could confuse future maintainers.
context "when upgrading from a date-tagged version to another (both have dates in tag)" do
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
docker/spec/dependabot/docker/update_checker_spec.rb:1914
- The HEAD request stubs for manifest digests in this test setup (lines 1911–1914) are never actually called. With version
"4.8.1-20251014-windowsservercore-ltsc2022"and theaspnet.jsonfixture (experiment enabled), the only compatible dated candidate after downgrade removal is the current tag itself ("4.8-20250909-windowsservercore-ltsc2022"is filtered out as a downgrade because its numeric version"4.8"is less than"4.8.1"). Since there are no newer candidates,select_tag_with_precisionnever callsdigest_ofand these stubs are never exercised. The comment"# Stub manifest digest requests"is therefore misleading. Consider removing the unused stubs and updating the comment to reflect the actual code path being tested.
# Stub manifest digest requests
stub_request(:head, repo_url + "manifests/4.8-20250909-windowsservercore-ltsc2022")
.and_return(status: 200, body: "", headers: JSON.parse(headers_response))
stub_request(:head, repo_url + "manifests/4.8.1-20251014-windowsservercore-ltsc2022")
.and_return(status: 200, body: "", headers: JSON.parse(headers_response.gsub("3ea1ca1", "5fb82b2")))
…mver compliant tags
…art of semver parsing.
…idered as a part of semver
…mp_validation experiment
…om the tag when comparing tags is now gated behind the experiment flag.
…tring, making sure that newer tags always match the same architecture regardless of if a different architecture variant is newer.
…t is on due to comparing only versions from the tag, stripping out the dates.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…nt tagged versions
…r casess where the experiment flag is off.
…validation scenarios
d290b9e to
f208f14
Compare
What are you trying to accomplish?
This PR updates the docker update checker to reference the docker image config metadata's
createdtag to determine exactly when a tag was last pushed to.It also updates tag parser to exclude dated tags when considering if a tag is a higher semantic version.
Both changes aregated behind the
docker_created_timestamp_validationexperiment flag.This fixes an issue seen when updating a docker image with a tag that when parsed strictly as semver, appears to be a higher version.
Ex: an image with the tag
4.8.1is updated to the tag4.8-20250909as the tag/semver parsing treats it as4.8.20250909which is a higher version than4.8.1Anything you want to highlight for special attention from reviewers?
I want to ensure no regressions occur with and without the experiment enabled. Ideally, after validation, we remove the experiment or enable it by default.
How will you know you've accomplished your goal?
If container image tag updates are still occurring, with no regressions that is a success.
Checklist