dependabot-docker 0.382.0 → 0.383.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9c3c3a9d65ba79f649da1e2078a73d446587e67b3b78ccca293710fd8a08c417
4
- data.tar.gz: d6862a5fd24ceab65a80f42b555d0d2219585a371464b7774c52f5b369dcc3aa
3
+ metadata.gz: '0200408166a023ea4720b018f90dda3232304f7b5f1abec28dd8dbb9bf2b3f0e'
4
+ data.tar.gz: 99f2b3c7a81588a396ca4fc38b1cb9ce9d5907f455122f7b3285fc84c92afe51
5
5
  SHA512:
6
- metadata.gz: 73acf20b4b759b7987898616686b1edbb423541b0b7c259c4568f8fd66c4b5c336cb7dc8fdbf83e1e054a458a4a3f0d04c9d6b605180416ba6ccfa5092ca6b59
7
- data.tar.gz: d674d576671aa6d270fc8d80db56c62dc46cb026f38342e3fdc83ede3204a517c7d4960d503cd9c45641d10cb7f10e6076c62488c7a2c53b0db4464facce0a3a
6
+ metadata.gz: b4eb568dd7249d487a42415564920512f63d953049334e64ec09171435385b48365e0934c8d392b8d7033d743f33b887fb4de1548e1c25db65e355cd2e9cf798
7
+ data.tar.gz: 517a96c97e6b74ed9873be9d49cbdf0913204f2111f4191c18a22a9c6c9e37c9665760a70662f84e4c87b65607b710414d871109fd72dc68d64f58f0f56795ea
@@ -263,6 +263,7 @@ module Dependabot
263
263
  # (which requires a call to the registry for each tag, so can be slow)
264
264
  candidate_tags = comparable_tags_from_registry(version_tag)
265
265
  candidate_tags = remove_version_downgrades(candidate_tags, version_tag)
266
+ candidate_tags = remove_more_precise_tags(candidate_tags, version_tag)
266
267
  candidate_tags = remove_prereleases(candidate_tags, version_tag)
267
268
  candidate_tags = filter_ignored(candidate_tags)
268
269
  candidate_tags = sort_tags(candidate_tags, version_tag)
@@ -285,14 +286,26 @@ module Dependabot
285
286
  candidate_tags.reverse_each do |candidate|
286
287
  selected = select_tag_with_precision(candidate, same_precision_tags, version_tag)
287
288
 
289
+ # Content check (runs regardless of experiments): if the candidate tag
290
+ # resolves to the exact same image contents as the current tag, there
291
+ # is no real update to make. This catches rolling tags (e.g. "9.0")
292
+ # that point to the same image as a pinned tag (e.g. "9.0.11").
293
+ if selected.name != version_tag.name && same_image_contents?(selected, version_tag)
294
+ Dependabot.logger.info(
295
+ "Digest check: #{selected.name} has the same image contents as #{version_tag.name} " \
296
+ "— no update needed, staying on #{version_tag.name}"
297
+ )
298
+ next
299
+ end
300
+
288
301
  if Dependabot::Experiments.enabled?(:docker_created_timestamp_validation) &&
289
302
  selected.name != version_tag.name
290
303
  if validation_attempts >= MAX_PLATFORM_VALIDATION_ATTEMPTS
291
304
  Dependabot.logger.info(
292
305
  "Platform validation: reached max attempts (#{MAX_PLATFORM_VALIDATION_ATTEMPTS}), " \
293
- "accepting #{selected.name} without timestamp check"
306
+ "skipping #{selected.name} continuing to find a validated candidate"
294
307
  )
295
- return selected
308
+ next
296
309
  end
297
310
  validation_attempts += 1
298
311
  end
@@ -562,6 +575,26 @@ module Dependabot
562
575
  end
563
576
  end
564
577
 
578
+ # When the current tag pins only the major or major.minor version (e.g.
579
+ # "9.0"), a candidate that additionally specifies a patch version (e.g.
580
+ # "9.0.17") is not a wanted upgrade: pinning the less precise tag opts into
581
+ # a rolling tag, so the patch should not be specified for them. Only true
582
+ # semver tags (":normal" format) are filtered here — build-number and date
583
+ # based formats have their own comparability rules and are left untouched.
584
+ sig do
585
+ params(
586
+ candidate_tags: T::Array[Dependabot::Docker::Tag],
587
+ version_tag: Dependabot::Docker::Tag
588
+ )
589
+ .returns(T::Array[Dependabot::Docker::Tag])
590
+ end
591
+ def remove_more_precise_tags(candidate_tags, version_tag)
592
+ return candidate_tags unless version_tag.format == :normal
593
+ return candidate_tags if version_tag.precision > 2
594
+
595
+ candidate_tags.select { |tag| tag.precision <= version_tag.precision }
596
+ end
597
+
565
598
  sig do
566
599
  params(
567
600
  candidate_tags: T::Array[Dependabot::Docker::Tag],
@@ -1083,6 +1116,80 @@ module Dependabot
1083
1116
  candidate_created > current_created
1084
1117
  end
1085
1118
 
1119
+ # Returns true when the candidate tag and current tag reference the exact
1120
+ # same image contents. Only multi-arch (manifest list) images are compared
1121
+ # here, by checking that both tags expose the identical set of platforms
1122
+ # with matching per-platform manifest digests. For single-platform images this
1123
+ # returns false (empty digest map), so this check is effectively a no-op and
1124
+ # we fall back to the existing semver/precision selection logic.
1125
+ sig do
1126
+ params(
1127
+ candidate_tag: Dependabot::Docker::Tag,
1128
+ current_tag: Dependabot::Docker::Tag
1129
+ ).returns(T::Boolean)
1130
+ end
1131
+ def same_image_contents?(candidate_tag, current_tag)
1132
+ current_digests = fetch_platform_digests(current_tag.name)
1133
+ return false if current_digests.empty?
1134
+
1135
+ candidate_digests = fetch_platform_digests(candidate_tag.name)
1136
+ return false if candidate_digests.empty?
1137
+
1138
+ # The contents are unchanged only when both tags expose the exact same set
1139
+ # of platforms with identical per-platform manifest digests. A candidate
1140
+ # with extra platforms is a different image and must not match.
1141
+ current_digests == candidate_digests
1142
+ end
1143
+
1144
+ # Fetches a map of platform key (e.g. "linux/amd64") to platform manifest
1145
+ # digest for a tag's manifest list. Returns an empty hash for single-platform
1146
+ # images or when the manifest can't be fetched.
1147
+ sig { params(tag_name: String).returns(T::Hash[String, String]) }
1148
+ def fetch_platform_digests(tag_name)
1149
+ return T.must(platform_digests_cache[tag_name]) if platform_digests_cache.key?(tag_name)
1150
+
1151
+ digests = fetch_platform_digests_from_registry(tag_name)
1152
+ platform_digests_cache[tag_name] = digests
1153
+ digests
1154
+ rescue *transient_docker_errors, DockerRegistry2::RegistryAuthenticationException,
1155
+ RestClient::Forbidden, RestClient::TooManyRequests, JSON::ParserError => e
1156
+ Dependabot.logger.info(
1157
+ "Failed to fetch platform digests for #{docker_repo_name}:#{tag_name}: #{e.message}"
1158
+ )
1159
+ platform_digests_cache[tag_name] = {}
1160
+ {}
1161
+ end
1162
+
1163
+ sig { params(tag_name: String).returns(T::Hash[String, String]) }
1164
+ def fetch_platform_digests_from_registry(tag_name)
1165
+ manifest = with_retries(max_attempts: 3, errors: transient_docker_errors) do
1166
+ docker_registry_client.manifest(docker_repo_name, tag_name)
1167
+ end
1168
+
1169
+ media_type = manifest["mediaType"] || manifest[:mediaType]
1170
+ return {} unless MANIFEST_LIST_TYPES.include?(media_type)
1171
+
1172
+ manifests = manifest["manifests"] || manifest[:manifests] || []
1173
+ collect_platform_digests(manifests)
1174
+ end
1175
+
1176
+ sig { params(manifests: T.untyped).returns(T::Hash[String, String]) }
1177
+ def collect_platform_digests(manifests)
1178
+ digests = {}
1179
+
1180
+ manifests.each do |m|
1181
+ platform = extract_platform(m)
1182
+ next unless platform
1183
+
1184
+ digest = m["digest"] || m[:digest]
1185
+ next unless digest
1186
+
1187
+ digests[platform_key(platform)] = digest
1188
+ end
1189
+
1190
+ digests
1191
+ end
1192
+
1086
1193
  # Fetches the platform entries from a manifest list for a given tag.
1087
1194
  # Returns nil if the tag is a single-platform image (not a manifest list).
1088
1195
  sig { params(tag_name: String).returns(T.nilable(T::Array[T::Hash[String, T.untyped]])) }
@@ -1218,6 +1325,14 @@ module Dependabot
1218
1325
  )
1219
1326
  end
1220
1327
 
1328
+ sig { returns(T::Hash[String, T::Hash[String, String]]) }
1329
+ def platform_digests_cache
1330
+ @platform_digests_cache ||= T.let(
1331
+ {},
1332
+ T.nilable(T::Hash[String, T::Hash[String, String]])
1333
+ )
1334
+ end
1335
+
1221
1336
  sig { returns(T::Hash[String, T.nilable(Time)]) }
1222
1337
  def config_created_timestamps
1223
1338
  @config_created_timestamps ||= T.let(
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dependabot-docker
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.382.0
4
+ version: 0.383.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dependabot
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 0.382.0
18
+ version: 0.383.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - '='
24
24
  - !ruby/object:Gem::Version
25
- version: 0.382.0
25
+ version: 0.383.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: debug
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -266,7 +266,7 @@ licenses:
266
266
  - MIT
267
267
  metadata:
268
268
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
269
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.382.0
269
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.383.0
270
270
  rdoc_options: []
271
271
  require_paths:
272
272
  - lib