Skip to content

Enhance Docker update checker to handle non-semver tags#14337

Merged
jpinz merged 20 commits intodependabot:mainfrom
jpinz:dev/jpinz/docker-tag-date-check
Mar 3, 2026
Merged

Enhance Docker update checker to handle non-semver tags#14337
jpinz merged 20 commits intodependabot:mainfrom
jpinz:dev/jpinz/docker-tag-date-check

Conversation

@jpinz
Copy link
Contributor

@jpinz jpinz commented Mar 2, 2026

What are you trying to accomplish?

This PR updates the docker update checker to reference the docker image config metadata's created tag 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_validation experiment 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.1 is updated to the tag 4.8-20250909 as the tag/semver parsing treats it as 4.8.20250909 which is a higher version than 4.8.1

Anything 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

  • I have run the complete test suite to ensure all tests and linters pass.
  • I have thoroughly tested my code changes to ensure they work as expected, including adding additional tests for new functionality.
  • I have written clear and descriptive commit messages.
  • I have provided a detailed description of the changes in the pull request, including the problem it addresses, how it fixes the problem, and any relevant details about the implementation.
  • I have ensured that the code is well-documented and easy to understand.

@jpinz jpinz self-assigned this Mar 2, 2026
Copilot AI review requested due to automatic review settings March 2, 2026 19:26
@jpinz jpinz requested a review from a team as a code owner March 2, 2026 19:26
@github-actions github-actions bot added the L: docker Docker containers label Mar 2, 2026
brbayes-msft
brbayes-msft previously approved these changes Mar 2, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. A dated_version? method on Tag that detects 8-digit date components (YYYYMMDD) to prevent cross-updates between dated and non-dated tags.
  2. A fetch_image_config_created method that reads the Docker image config blob's created timestamp to verify actual image recency when semver comparison is misleading.

Changes:

  • Refactors version_related_pattern? into a class-level constant VERSION_RELATED_PATTERNS with updated matching logic (removing the old broad prerelease pattern and rc/jre markers, and adding a digit-leading mixed pattern for PEP 440 prereleases).
  • Adds timestamp validation via new validate_tag_with_timestamp, fetch_image_config_created, and resolve_platform_manifest methods to the UpdateChecker.
  • Adds dated_version? and comparable_to? changes to Tag so 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_registry and resolve_platform_manifest methods contain non-trivial logic (manifest list resolution, platform selection preferring amd64, config blob parsing, and Time.parse for the created field) but all tests stub fetch_image_config_created at the higher level. There are no tests exercising the actual registry interaction path for these methods. Adding unit tests for resolve_platform_manifest (e.g., with a manifest list input vs single-platform input) and the full fetch_image_config_created path 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_timestamp may call fetch_image_config_created for 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

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 describe context description at line 1715 says "when upgrading from a date-tagged version to another (both have dates in tag)", but the test body asserts latest_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

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 the aspnet.json fixture (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_precision never calls digest_of and 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")))

@JamieMagee JamieMagee force-pushed the dev/jpinz/docker-tag-date-check branch from d290b9e to f208f14 Compare March 3, 2026 22:03
@jpinz jpinz merged commit 57ab757 into dependabot:main Mar 3, 2026
55 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

L: docker Docker containers

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants