dependabot-devcontainers 0.317.0 → 0.318.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: 359ea7ce7d31cf9697ec9ad2a93c49708e352b61a8ac549a7b717db97d5c4695
4
- data.tar.gz: cc2c965edcff88f6e6936a127d0466bd95b003e1ff3052a19136a9a3eff506f6
3
+ metadata.gz: de9793d223415fa88f25d8b0f21aff4459ab698e3bbb8ddc9fc7ac62ac036b53
4
+ data.tar.gz: 9a0ae4b5d3b0036d6b182ed19a331687b4227c08f116271b7d76565145fceca9
5
5
  SHA512:
6
- metadata.gz: 17beba0287560f94f88fb11f0699a019d527fd090662bee985b0595c5686665c9e300e3cf49cffdf2db220a635a70384f39e171f0d53f3e9a1a22922732c51c6
7
- data.tar.gz: 8937c503484c97aa861ed7f88eb948fd9386c09b47a03db871b660c6d683dd4a4023e69d5f440bba540950ef316aea7b3382df770dd4efd3917c7f61f35faa52
6
+ metadata.gz: 1f9d44c19e7f5eebff0f81d3125d9a2cc23ebef7e62972a0f85f9bb0be2c845e01d3add6f42c0f83a48334f5ac0ed53f1d738ff84792c1d21d6bfcccad9e6daa
7
+ data.tar.gz: 90241270d8b5eaa2ea652e6abf3423475618ab6a17c91b4a6e620868d1a111c66dcc09069648441a5c53d07c2bee247cd59d9663addf88dd315208e9ce3702e5
@@ -0,0 +1,136 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+ require "time"
6
+ require "cgi"
7
+ require "excon"
8
+ require "nokogiri"
9
+ require "sorbet-runtime"
10
+ require "dependabot/registry_client"
11
+ require "dependabot/devcontainers"
12
+ require "dependabot/devcontainers/version"
13
+ require "dependabot/package/package_release"
14
+ require "dependabot/package/package_details"
15
+
16
+ module Dependabot
17
+ module Devcontainers
18
+ module Package
19
+ class PackageDetailsFetcher
20
+ extend T::Sig
21
+
22
+ sig do
23
+ params(
24
+ dependency: Dependabot::Dependency
25
+ ).void
26
+ end
27
+ def initialize(dependency:)
28
+ @dependency = dependency
29
+ end
30
+
31
+ sig { returns(Dependabot::Dependency) }
32
+ attr_reader :dependency
33
+
34
+ sig { returns(T.nilable(T::Array[Dependabot::Package::PackageRelease])) }
35
+ def fetch_package_releases
36
+ releases = T.let([], T::Array[Dependabot::Package::PackageRelease])
37
+
38
+ begin
39
+ Dependabot.logger.info("Fetching package info (Dev Containers) for #{dependency.name}")
40
+
41
+ cmd = "devcontainer features info tags #{dependency.name} --output-format json"
42
+ Dependabot.logger.info("Running command: `#{cmd}`")
43
+
44
+ output = SharedHelpers.run_shell_command(cmd, stderr_to_stdout: false)
45
+ package_metadata = JSON.parse(output).fetch("publishedTags")
46
+
47
+ package_metadata.each do |release|
48
+ next unless version_class.correct?(release) &&
49
+ T.cast(version_class.new(release), Dependabot::Devcontainers::Version)
50
+
51
+ releases << Dependabot::Package::PackageRelease.new(
52
+ version: Devcontainers::Version.new(release),
53
+ released_at: nil
54
+ )
55
+ end
56
+
57
+ releases.sort_by!(&:version)
58
+ rescue StandardError => e
59
+ Dependabot.logger.error("Error while fetching package info for dev container packages: #{e.message}")
60
+ releases
61
+ end
62
+ end
63
+
64
+ sig do
65
+ params(release: Dependabot::Package::PackageRelease).returns(Dependabot::Package::PackageRelease)
66
+ end
67
+ def fetch_release_metadata(release:)
68
+ Dependabot.logger.info("Fetching release metadata (Dev Containers) for #{dependency.name}:#{release.version}")
69
+
70
+ response = fetch_response(release: release)
71
+ return release unless response.status == 200
72
+
73
+ metadata_json = JSON.parse(response.body)["layers"][0]
74
+ released_at = metadata_json["annotations"]["org.opencontainers.image.created"]
75
+
76
+ release.instance_variable_set(:@tag, metadata_json["digest"])
77
+
78
+ # Annotations properties are optional, So getting a release date is not guaranteed.
79
+ # https://specs.opencontainers.org/image-spec/annotations/#annotations
80
+ release.instance_variable_set(:@released_at,
81
+ released_at ? Time.parse(released_at) : nil)
82
+
83
+ release
84
+ rescue StandardError => e
85
+ Dependabot.logger.error("Error while fetching metadata (Dev Container) " \
86
+ "for #{dependency.name}: #{e.message}")
87
+ release
88
+ end
89
+
90
+ private
91
+
92
+ sig { params(release: Dependabot::Package::PackageRelease).returns(Excon::Response) }
93
+ def fetch_response(release:)
94
+ url = "https://#{ref_registry}/v2/#{ref_namespace}/#{ref_id}/manifests/#{release.version}"
95
+
96
+ Dependabot::RegistryClient.get(url: url,
97
+ headers: { "Accept" => "application/vnd.oci.image.manifest.v1+json",
98
+ "user-agent": "devcontainer",
99
+ "Authorization" => "Bearer #{auth_token}" })
100
+ end
101
+
102
+ sig { returns(T.class_of(Dependabot::Version)) }
103
+ def version_class
104
+ dependency.version_class
105
+ end
106
+
107
+ sig { returns(String) }
108
+ def ref_registry
109
+ T.must(dependency.name.split("/")[0])
110
+ end
111
+
112
+ sig { returns(String) }
113
+ def ref_namespace
114
+ T.must(dependency.name.split("/")[1...-1]).join("/")
115
+ end
116
+
117
+ sig { returns(String) }
118
+ def ref_id
119
+ T.must(dependency.name.split("/")[-1])
120
+ end
121
+
122
+ # ghcr.io needs a auth token to access the package metadata, We use following
123
+ # token system to fetch on the fly auth token for authentication.
124
+ # https://docs.docker.com/registry/spec/auth/token/#how-to-authenticate
125
+ # the idea was borrowed from https://github.com/devcontainers/cli project
126
+ sig { returns(T.nilable(String)) }
127
+ def auth_token
128
+ token_url = "https://#{ref_registry}/token?service=ghcr.io&scope=repository:#{ref_namespace}/#{ref_id}:pull"
129
+
130
+ response = Excon.get(token_url, headers: { "Accept" => "application/json" })
131
+ JSON.parse(response.body)["token"]
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,177 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "excon"
5
+ require "json"
6
+ require "sorbet-runtime"
7
+
8
+ require "open3"
9
+ require "shellwords"
10
+ require "dependabot/errors"
11
+ require "dependabot/package/package_latest_version_finder"
12
+ require "dependabot/shared_helpers"
13
+ require "dependabot/update_checkers/version_filters"
14
+ require "dependabot/devcontainers/file_parser"
15
+ require "dependabot/devcontainers/package/package_details_fetcher"
16
+ require "dependabot/devcontainers/requirement"
17
+ require "dependabot/devcontainers/update_checker"
18
+
19
+ module Dependabot
20
+ module Devcontainers
21
+ class UpdateChecker
22
+ class LatestVersionFinder < Dependabot::Package::PackageLatestVersionFinder
23
+ extend T::Sig
24
+
25
+ sig do
26
+ params(
27
+ dependency: Dependabot::Dependency,
28
+ dependency_files: T::Array[Dependabot::DependencyFile],
29
+ credentials: T::Array[Dependabot::Credential],
30
+ ignored_versions: T::Array[String],
31
+ security_advisories: T::Array[Dependabot::SecurityAdvisory],
32
+ raise_on_ignored: T::Boolean,
33
+ options: T::Hash[Symbol, T.untyped],
34
+ cooldown_options: T.nilable(Dependabot::Package::ReleaseCooldownOptions)
35
+ ).void
36
+ end
37
+ def initialize(
38
+ dependency:,
39
+ dependency_files:,
40
+ credentials:,
41
+ ignored_versions:,
42
+ security_advisories:,
43
+ raise_on_ignored:,
44
+ options: {},
45
+ cooldown_options: nil
46
+ )
47
+ @dependency = dependency
48
+ @dependency_files = dependency_files
49
+ @credentials = credentials
50
+ @ignored_versions = ignored_versions
51
+ @security_advisories = security_advisories
52
+ @raise_on_ignored = raise_on_ignored
53
+ @options = options
54
+ @cooldown_options = cooldown_options
55
+ super
56
+ end
57
+
58
+ sig { returns(Dependabot::Dependency) }
59
+ attr_reader :dependency
60
+ sig { returns(T::Array[Dependabot::Credential]) }
61
+ attr_reader :credentials
62
+ sig { returns(T.nilable(Dependabot::Package::ReleaseCooldownOptions)) }
63
+ attr_reader :cooldown_options
64
+ sig { returns(T::Array[String]) }
65
+ attr_reader :ignored_versions
66
+ sig { returns(T::Array[Dependabot::SecurityAdvisory]) }
67
+ attr_reader :security_advisories
68
+ sig { override.returns(T.nilable(Dependabot::Package::PackageDetails)) }
69
+ def package_details; end
70
+
71
+ sig { returns(T.nilable(T::Array[Dependabot::Version])) }
72
+ def release_versions
73
+ releases = package_releases
74
+
75
+ releases = filter_ignored_versions(T.must(releases))
76
+ releases = filter_lower_versions(releases)
77
+ releases = lazy_filter_cooldown_versions(releases)
78
+
79
+ releases = releases.sort_by(&:version)
80
+
81
+ if releases.empty?
82
+ Dependabot.logger.info("No release candidates found for #{dependency.name}, returning current version")
83
+ return Array(current_version)
84
+ end
85
+
86
+ releases.map(&:version)
87
+ end
88
+
89
+ private
90
+
91
+ sig { returns(T.nilable(T::Array[Dependabot::Package::PackageRelease])) }
92
+ def package_releases
93
+ @package_releases = T.let(Dependabot::Devcontainers::Package::PackageDetailsFetcher
94
+ .new(dependency: dependency)
95
+ .fetch_package_releases, T.nilable(T::Array[Dependabot::Package::PackageRelease]))
96
+ end
97
+
98
+ sig { override.returns(T::Boolean) }
99
+ def cooldown_enabled?
100
+ Dependabot::Experiments.enabled?(:enable_cooldown_for_devcontainers)
101
+ end
102
+
103
+ sig { returns(T.nilable(Dependabot::Version)) }
104
+ def current_version
105
+ @current_version ||=
106
+ T.let(
107
+ dependency.numeric_version,
108
+ T.nilable(Dependabot::Version)
109
+ )
110
+ end
111
+
112
+ sig do
113
+ params(releases: T::Array[Dependabot::Package::PackageRelease])
114
+ .returns(T::Array[Dependabot::Package::PackageRelease])
115
+ end
116
+ def lazy_filter_cooldown_versions(releases)
117
+ return releases unless cooldown_enabled?
118
+ return releases unless cooldown_options
119
+
120
+ Dependabot.logger.info("Initializing cooldown filter")
121
+
122
+ unless releases.any?
123
+ Dependabot.logger.info("No releases found, skipping cooldown filtering")
124
+ return releases
125
+ end
126
+
127
+ sorted_releases = releases.sort_by(&:version).reverse
128
+ filtered_versions = []
129
+ cooldown_filtered_versions = 0
130
+
131
+ sorted_releases.each do |release|
132
+ if in_cooldown_period?(release)
133
+ Dependabot.logger.info("Filtered out (cooldown) : #{release}")
134
+ cooldown_filtered_versions += 1
135
+ next
136
+ end
137
+
138
+ filtered_versions << release
139
+ break
140
+ end
141
+ Dependabot.logger.info("Filtered out #{cooldown_filtered_versions} version(s) due to cooldown")
142
+
143
+ filtered_versions
144
+ end
145
+
146
+ # rubocop:disable Metrics/AbcSize
147
+ sig { params(release: Dependabot::Package::PackageRelease).returns(T::Boolean) }
148
+ def in_cooldown_period?(release)
149
+ release = T.let(Dependabot::Devcontainers::Package::PackageDetailsFetcher
150
+ .new(dependency: dependency)
151
+ .fetch_release_metadata(release: release), T.nilable(Dependabot::Package::PackageRelease))
152
+
153
+ unless T.must(release).released_at
154
+ Dependabot.logger.info(
155
+ "Release date unavailable for #{T.must(release).version}. Cooldown filtering not possible"
156
+ )
157
+ return false
158
+ end
159
+
160
+ current_version = version_class.correct?(dependency.version) ? version_class.new(dependency.version) : nil
161
+
162
+ days = cooldown_days_for(current_version, T.must(release).version)
163
+ passed_seconds = Time.now.to_i - T.must(release).released_at.to_i
164
+ passed_days = passed_seconds / DAY_IN_SECONDS
165
+
166
+ if passed_days < days
167
+ Dependabot.logger.info("Version #{T.must(release).version}, Release date: #{T.must(release).released_at}." \
168
+ " Days since release: #{passed_days} (cooldown days: #{days})")
169
+ end
170
+
171
+ passed_seconds < days * DAY_IN_SECONDS
172
+ end
173
+ # rubocop:enable Metrics/AbcSize
174
+ end
175
+ end
176
+ end
177
+ end
@@ -14,9 +14,11 @@ module Dependabot
14
14
  class UpdateChecker < Dependabot::UpdateCheckers::Base
15
15
  extend T::Sig
16
16
 
17
+ require_relative "update_checker/latest_version_finder"
18
+
17
19
  sig { override.returns(T.nilable(T.any(String, Gem::Version))) }
18
20
  def latest_version
19
- @latest_version ||= T.let(fetch_latest_version, T.nilable(Gem::Version))
21
+ @latest_version ||= T.let(T.must(release_versions).last, T.nilable(Gem::Version))
20
22
  end
21
23
 
22
24
  sig { override.returns(T.nilable(T.any(String, Gem::Version))) }
@@ -24,12 +26,19 @@ module Dependabot
24
26
  latest_version # TODO
25
27
  end
26
28
 
29
+ sig { override.returns(T.nilable(Dependabot::Version)) }
30
+ def latest_resolvable_version_with_no_unlock
31
+ raise NotImplementedError
32
+ end
33
+
27
34
  sig { override.returns(T::Array[T::Hash[Symbol, T.untyped]]) }
28
35
  def updated_requirements
29
36
  dependency.requirements.map do |requirement|
30
37
  required_version = T.cast(version_class.new(requirement[:requirement]), Dependabot::Devcontainers::Version)
31
- updated_requirement = remove_precision_changes(viable_candidates, required_version).last
32
-
38
+ updated_requirement = remove_precision_changes(
39
+ T.cast(release_versions, T::Array[Dependabot::Devcontainers::Version]),
40
+ required_version
41
+ ).last
33
42
  {
34
43
  file: requirement[:file],
35
44
  requirement: updated_requirement,
@@ -39,35 +48,30 @@ module Dependabot
39
48
  end
40
49
  end
41
50
 
42
- sig { override.returns(T.nilable(Dependabot::Version)) }
43
- def latest_resolvable_version_with_no_unlock
44
- raise NotImplementedError
45
- end
46
-
47
51
  private
48
52
 
49
- sig { returns(T::Array[Dependabot::Devcontainers::Version]) }
50
- def viable_candidates
51
- @viable_candidates ||= T.let(
52
- fetch_viable_candidates,
53
- T.nilable(T::Array[Dependabot::Devcontainers::Version])
53
+ sig { returns(T.nilable(Dependabot::Devcontainers::UpdateChecker::LatestVersionFinder)) }
54
+ def latest_version_finder
55
+ @latest_version_finder ||= T.let(LatestVersionFinder.new(
56
+ dependency: dependency,
57
+ credentials: credentials,
58
+ dependency_files: dependency_files,
59
+ security_advisories: security_advisories,
60
+ ignored_versions: ignored_versions,
61
+ raise_on_ignored: raise_on_ignored,
62
+ cooldown_options: update_cooldown
63
+ ),
64
+ T.nilable(Dependabot::Devcontainers::UpdateChecker::LatestVersionFinder))
65
+ end
66
+
67
+ sig { returns(T.nilable(T::Array[Dependabot::Version])) }
68
+ def release_versions
69
+ @release_versions ||= T.let(
70
+ T.must(T.must(latest_version_finder).release_versions),
71
+ T.nilable(T::Array[Dependabot::Version])
54
72
  )
55
73
  end
56
74
 
57
- sig { returns(T::Array[Dependabot::Devcontainers::Version]) }
58
- def fetch_viable_candidates
59
- candidates = comparable_versions_from_registry
60
- candidates = filter_ignored(candidates)
61
- candidates.sort
62
- end
63
-
64
- sig { returns(Dependabot::Devcontainers::Version) }
65
- def fetch_latest_version
66
- return T.cast(current_version, Dependabot::Devcontainers::Version) unless viable_candidates.any?
67
-
68
- T.must(viable_candidates.last)
69
- end
70
-
71
75
  sig do
72
76
  params(
73
77
  versions: T::Array[Dependabot::Devcontainers::Version],
@@ -81,57 +85,6 @@ module Dependabot
81
85
  end
82
86
  end
83
87
 
84
- sig do
85
- params(
86
- versions: T::Array[Dependabot::Devcontainers::Version]
87
- )
88
- .returns(T::Array[Dependabot::Devcontainers::Version])
89
- end
90
- def filter_ignored(versions)
91
- filtered =
92
- versions.reject do |version|
93
- ignore_requirements.any? { |r| version.satisfies?(r) }
94
- end
95
-
96
- if raise_on_ignored &&
97
- filter_lower_versions(filtered).empty? &&
98
- filter_lower_versions(versions).any?
99
- raise AllVersionsIgnored
100
- end
101
-
102
- filtered
103
- end
104
-
105
- sig { returns(T::Array[Dependabot::Devcontainers::Version]) }
106
- def comparable_versions_from_registry
107
- tags_from_registry.filter_map do |tag|
108
- version_class.correct?(tag) && T.cast(version_class.new(tag), Dependabot::Devcontainers::Version)
109
- end
110
- end
111
-
112
- sig { returns(T::Array[String]) }
113
- def tags_from_registry
114
- @tags_from_registry ||= T.let(fetch_tags_from_registry, T.nilable(T::Array[String]))
115
- end
116
-
117
- sig { returns(T::Array[String]) }
118
- def fetch_tags_from_registry
119
- cmd = "devcontainer features info tags #{dependency.name} --output-format json"
120
-
121
- Dependabot.logger.info("Running command: `#{cmd}`")
122
-
123
- output = SharedHelpers.run_shell_command(cmd, stderr_to_stdout: false)
124
-
125
- JSON.parse(output).fetch("publishedTags")
126
- end
127
-
128
- sig { params(versions: T::Array[Gem::Version]).returns(T::Array[Gem::Version]) }
129
- def filter_lower_versions(versions)
130
- versions.select do |version|
131
- version > current_version
132
- end
133
- end
134
-
135
88
  sig { override.returns(T::Boolean) }
136
89
  def latest_version_resolvable_with_full_unlock?
137
90
  false # TODO
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dependabot-devcontainers
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.317.0
4
+ version: 0.318.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.317.0
18
+ version: 0.318.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.317.0
25
+ version: 0.318.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: debug
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -248,9 +248,11 @@ files:
248
248
  - lib/dependabot/devcontainers/file_updater/config_updater.rb
249
249
  - lib/dependabot/devcontainers/language.rb
250
250
  - lib/dependabot/devcontainers/metadata_finder.rb
251
+ - lib/dependabot/devcontainers/package/package_details_fetcher.rb
251
252
  - lib/dependabot/devcontainers/package_manager.rb
252
253
  - lib/dependabot/devcontainers/requirement.rb
253
254
  - lib/dependabot/devcontainers/update_checker.rb
255
+ - lib/dependabot/devcontainers/update_checker/latest_version_finder.rb
254
256
  - lib/dependabot/devcontainers/utils.rb
255
257
  - lib/dependabot/devcontainers/version.rb
256
258
  homepage: https://github.com/dependabot/dependabot-core
@@ -258,7 +260,7 @@ licenses:
258
260
  - MIT
259
261
  metadata:
260
262
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
261
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.317.0
263
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.318.0
262
264
  rdoc_options: []
263
265
  require_paths:
264
266
  - lib