dependabot-common 0.387.0 → 0.388.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: 1af9eae7cb945bd312cf8a9fee63b2f4e42adcbc3d137208e2043b659052013d
4
- data.tar.gz: f815b9a6961d5ecbd9c77ffdf296a9a048dbfadf21fae5ddebc4bbd8db6fbd2b
3
+ metadata.gz: 29f29c96dbb5e187d5cdecfe015ecd60456ea004a7553a97b812416034451ebf
4
+ data.tar.gz: 1d6e5d04baaf18853476f074e742eb8a60113e2fdfa457db4b3019b1055b30ea
5
5
  SHA512:
6
- metadata.gz: 34737076727da04d065d0163c93fe1058cfbb3a86d79d2cfef2be4cf126d2fd4d335e204087d3c337e2d97f5ec0220878a1110e139f553805c4d36574e33cb5d
7
- data.tar.gz: 5885a478a01996a6f382b3c2bedb29ff59ca557c0316582f14e9136bd3881dac04d20874862bc76a93a16119653a85c868d3d08c65e8cabb9fff1ae257124323
6
+ metadata.gz: 2c2d09853e0044b7dd6ed6ca9f56501ee8436f113eecec3d85506d1543b79994cb3544cb5718b6f55960f7257af74848e33bc330d956275f98a2d8220f09d4d6
7
+ data.tar.gz: 343ab8ddca784ef0e497987de868f0b405aeeb3bfb8f11c92ad24da3b6d067981fde09397e4d6a5633e2556bfa527f594f432dc4ca9aa79cd9b2b556be930e52
@@ -0,0 +1,61 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "sawyer"
5
+ require "sorbet-runtime"
6
+ require "time"
7
+
8
+ module Dependabot
9
+ module Clients
10
+ # Typed view over the release fields returned by Octokit.
11
+ class GithubRelease < T::ImmutableStruct
12
+ extend T::Sig
13
+
14
+ const :id, T.nilable(Integer)
15
+ const :name, T.nilable(String)
16
+ const :tag_name, String
17
+ const :body, T.nilable(String)
18
+ const :html_url, T.nilable(String)
19
+ const :prerelease, T::Boolean
20
+ const :published_at, T.nilable(Time)
21
+
22
+ sig { params(resource: Sawyer::Resource).returns(T.nilable(GithubRelease)) }
23
+ def self.from_resource(resource)
24
+ tag_name = T.cast(resource[:tag_name], Object)
25
+ return unless tag_name.is_a?(String)
26
+
27
+ id = T.cast(resource[:id], Object)
28
+ prerelease = T.cast(resource[:prerelease], Object)
29
+
30
+ new(
31
+ id: id.is_a?(Integer) ? id : nil,
32
+ name: string_value(resource, :name),
33
+ tag_name: tag_name,
34
+ body: string_value(resource, :body),
35
+ html_url: string_value(resource, :html_url),
36
+ prerelease: prerelease == true,
37
+ published_at: time_value(resource, :published_at)
38
+ )
39
+ end
40
+
41
+ sig { params(resource: Sawyer::Resource, key: Symbol).returns(T.nilable(String)) }
42
+ def self.string_value(resource, key)
43
+ value = T.cast(resource[key], Object)
44
+ value.is_a?(String) ? value : nil
45
+ end
46
+ private_class_method :string_value
47
+
48
+ sig { params(resource: Sawyer::Resource, key: Symbol).returns(T.nilable(Time)) }
49
+ def self.time_value(resource, key)
50
+ value = T.cast(resource[key], Object)
51
+ return value if value.is_a?(Time)
52
+ return unless value.is_a?(String)
53
+
54
+ Time.parse(value)
55
+ rescue ArgumentError
56
+ nil
57
+ end
58
+ private_class_method :time_value
59
+ end
60
+ end
61
+ end
@@ -1,4 +1,4 @@
1
- # typed: strict
1
+ # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "sorbet-runtime"
@@ -33,18 +33,23 @@ module Dependabot
33
33
  extend T::Sig
34
34
  extend T::Generic
35
35
 
36
- # The values of a requirement entry are heterogeneous and
37
- # ecosystem-specific, so this bridge class is necessarily untyped at
38
- # the Hash level; the typed readers below are the migration path.
39
- # rubocop:disable Sorbet/ForbidTUntyped
36
+ Group = T.type_alias { T.any(String, Symbol) }
37
+ ObjectHash = T.type_alias { T::Hash[T.any(Symbol, String), Object] }
38
+ Requirement = T.type_alias { T.any(String, Symbol) }
39
+ Input = T.type_alias { T.any(DependencyRequirement, ObjectHash) }
40
+
40
41
  K = type_member { { fixed: Symbol } }
42
+ # Hash-style access remains dynamic until ecosystem callers migrate to the
43
+ # typed readers. Keeping that compatibility here avoids a flag-day change.
44
+ # rubocop:disable Sorbet/ForbidTUntyped
41
45
  V = type_member { { fixed: T.untyped } }
42
46
  Elem = type_member { { fixed: [Symbol, T.untyped] } }
47
+ # rubocop:enable Sorbet/ForbidTUntyped
43
48
 
44
49
  # Builds a DependencyRequirement from a requirement hash, symbolising
45
50
  # top-level keys. Accepts both plain hashes and existing
46
51
  # DependencyRequirement instances and always returns a new instance.
47
- sig { params(hash: T::Hash[T.any(Symbol, String), T.untyped]).returns(DependencyRequirement) }
52
+ sig { params(hash: Input).returns(DependencyRequirement) }
48
53
  def self.create(hash)
49
54
  requirement = new
50
55
  requirement.replace(hash.keys.to_h { |k| [k.to_sym, hash[k]] })
@@ -52,16 +57,21 @@ module Dependabot
52
57
  end
53
58
 
54
59
  # The version constraint string, e.g. ">= 1.0, < 2.0". Nil when the
55
- # dependency is pinned by a lockfile rather than a manifest constraint.
56
- sig { returns(T.nilable(String)) }
60
+ # dependency is pinned by a lockfile rather than a manifest constraint,
61
+ # or :unfixable when no valid updated requirement can be generated.
62
+ sig { returns(T.nilable(Requirement)) }
57
63
  def requirement
58
- self[:requirement]
64
+ value = T.cast(self[:requirement], T.nilable(Object))
65
+ return if value.nil?
66
+ return value if value.is_a?(String) || value == :unfixable
67
+
68
+ raise TypeError, "requirement must be a string, :unfixable, or nil"
59
69
  end
60
70
 
61
71
  # The manifest file this requirement was declared in, e.g. "Gemfile".
62
72
  sig { returns(T.nilable(String)) }
63
73
  def file
64
- self[:file]
74
+ optional_string(:file)
65
75
  end
66
76
 
67
77
  # The dependency groups this requirement belongs to, e.g. ["dev"] or
@@ -69,26 +79,61 @@ module Dependabot
69
79
  # Nilable because some requirement entries are constructed with
70
80
  # groups: nil, and the reader must reflect that to stay a drop-in for
71
81
  # the underlying hash access under sorbet-runtime.
72
- sig { returns(T.nilable(T::Array[T.untyped])) }
82
+ sig { returns(T.nilable(T::Array[Group])) }
73
83
  def groups
74
- self[:groups]
84
+ value = T.cast(self[:groups], T.nilable(Object))
85
+ return if value.nil?
86
+ raise TypeError, "groups must be an array of strings or symbols, or nil" unless value.is_a?(Array)
87
+
88
+ value.each do |raw_group|
89
+ group = T.cast(raw_group, Object)
90
+ next if group.is_a?(String) || group.is_a?(Symbol)
91
+
92
+ raise TypeError, "groups must be an array of strings or symbols, or nil"
93
+ end
94
+ value
75
95
  end
76
96
 
77
97
  # The source details for the dependency, e.g.
78
98
  # { type: "git", url: "https://github.com/..." }. Keys may be symbols
79
99
  # or strings depending on whether the requirement was built by a file
80
100
  # parser or deserialised from a job definition.
81
- sig { returns(T.nilable(T::Hash[T.any(Symbol, String), T.untyped])) }
101
+ sig { returns(T.nilable(ObjectHash)) }
82
102
  def source
83
- self[:source]
103
+ optional_object_hash(:source)
84
104
  end
85
105
 
86
106
  # Optional ecosystem-specific metadata about the requirement, e.g.
87
107
  # { property_name: "rails.version" }.
88
- sig { returns(T.nilable(T::Hash[T.any(Symbol, String), T.untyped])) }
108
+ sig { returns(T.nilable(ObjectHash)) }
89
109
  def metadata
90
- self[:metadata]
110
+ optional_object_hash(:metadata)
111
+ end
112
+
113
+ private
114
+
115
+ sig { params(key: Symbol).returns(T.nilable(String)) }
116
+ def optional_string(key)
117
+ value = T.cast(self[key], T.nilable(Object))
118
+ return if value.nil?
119
+ return value if value.is_a?(String)
120
+
121
+ raise TypeError, "#{key} must be a string or nil"
122
+ end
123
+
124
+ sig { params(key: Symbol).returns(T.nilable(ObjectHash)) }
125
+ def optional_object_hash(key)
126
+ value = T.cast(self[key], T.nilable(Object))
127
+ return if value.nil?
128
+ raise TypeError, "#{key} must be a hash with string or symbol keys, or nil" unless value.is_a?(Hash)
129
+
130
+ value.each_key do |raw_nested_key|
131
+ nested_key = T.cast(raw_nested_key, Object)
132
+ next if nested_key.is_a?(String) || nested_key.is_a?(Symbol)
133
+
134
+ raise TypeError, "#{key} must be a hash with string or symbol keys, or nil"
135
+ end
136
+ value
91
137
  end
92
- # rubocop:enable Sorbet/ForbidTUntyped
93
138
  end
94
139
  end
@@ -0,0 +1,51 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ module Dependabot
7
+ class ErrorDetails < T::ImmutableStruct
8
+ extend T::Sig
9
+
10
+ DetailHash = T.type_alias { T::Hash[T.any(String, Symbol), T.anything] }
11
+ Detail = T.type_alias { T.any(String, DetailHash) }
12
+
13
+ const :error_type, String
14
+ const :error_detail, T.nilable(Detail)
15
+
16
+ sig { params(hash: T::Hash[Symbol, T.anything]).returns(ErrorDetails) }
17
+ def self.from_hash(hash)
18
+ raw_type = T.cast(hash[:"error-type"], Object)
19
+ raise TypeError, "error-type must be a string" unless raw_type.is_a?(String)
20
+
21
+ new(
22
+ error_type: raw_type,
23
+ error_detail: parse_detail(T.cast(hash[:"error-detail"], T.nilable(Object)))
24
+ )
25
+ end
26
+
27
+ sig { returns(T::Hash[Symbol, Object]) }
28
+ def to_h
29
+ result = T.let({ "error-type": error_type }, T::Hash[Symbol, Object])
30
+ result[:"error-detail"] = error_detail if error_detail
31
+ result
32
+ end
33
+
34
+ sig { params(value: T.nilable(Object)).returns(T.nilable(Detail)) }
35
+ def self.parse_detail(value)
36
+ return if value.nil?
37
+ return value if value.is_a?(String)
38
+ raise TypeError, "error-detail must be a string or hash" unless value.is_a?(Hash)
39
+
40
+ result = T.let({}, DetailHash)
41
+ value.each do |raw_key, raw_value|
42
+ key = T.cast(raw_key, Object)
43
+ raise TypeError, "error-detail keys must be strings or symbols" unless key.is_a?(String) || key.is_a?(Symbol)
44
+
45
+ result[key] = T.cast(raw_value, Object)
46
+ end
47
+ result
48
+ end
49
+ private_class_method :parse_detail
50
+ end
51
+ end
@@ -2,6 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "sorbet-runtime"
5
+ require "dependabot/error_details"
5
6
  require "dependabot/utils"
6
7
 
7
8
  # rubocop:disable Metrics/ModuleLength
@@ -23,7 +24,7 @@ module Dependabot
23
24
 
24
25
  # rubocop:disable Metrics/MethodLength
25
26
  # rubocop:disable Metrics/CyclomaticComplexity
26
- sig { params(error: StandardError).returns(T.nilable(T::Hash[Symbol, T.anything])) }
27
+ sig { params(error: StandardError).returns(T.nilable(Dependabot::ErrorDetails)) }
27
28
  def self.fetcher_error_details(error)
28
29
  case error
29
30
  when Dependabot::ToolVersionNotSupported
@@ -138,11 +139,12 @@ module Dependabot
138
139
  "rate-limit-reset": T.cast(error, Octokit::Error).response_headers["X-RateLimit-Reset"]
139
140
  }
140
141
  }
141
- end
142
+ end => details
143
+ Dependabot::ErrorDetails.from_hash(details) if details
142
144
  end
143
145
  # rubocop:enable Metrics/CyclomaticComplexity
144
146
 
145
- sig { params(error: StandardError).returns(T.nilable(T::Hash[Symbol, T.anything])) }
147
+ sig { params(error: StandardError).returns(T.nilable(Dependabot::ErrorDetails)) }
146
148
  def self.parser_error_details(error)
147
149
  case error
148
150
  when Dependabot::ToolFeatureNotSupported
@@ -220,13 +222,14 @@ module Dependabot
220
222
  # and responsibility for fixing it is on them, not us. As a result we
221
223
  # quietly log these as errors
222
224
  { "error-type": "server_error" }
223
- end
225
+ end => details
226
+ Dependabot::ErrorDetails.from_hash(details) if details
224
227
  end
225
228
 
226
229
  # rubocop:disable Lint/RedundantCopDisableDirective
227
230
  # rubocop:disable Metrics/CyclomaticComplexity
228
231
  # rubocop:disable Metrics/AbcSize
229
- sig { params(error: StandardError).returns(T.nilable(T::Hash[Symbol, T.anything])) }
232
+ sig { params(error: StandardError).returns(T.nilable(Dependabot::ErrorDetails)) }
230
233
  def self.updater_error_details(error)
231
234
  case error
232
235
  when Dependabot::ToolFeatureNotSupported
@@ -400,7 +403,8 @@ module Dependabot
400
403
  "rate-limit-reset": T.cast(error, Octokit::Error).response_headers["X-RateLimit-Reset"]
401
404
  }
402
405
  }
403
- end
406
+ end => details
407
+ Dependabot::ErrorDetails.from_hash(details) if details
404
408
  end
405
409
 
406
410
  # rubocop:enable Metrics/MethodLength
@@ -70,7 +70,7 @@ module Dependabot
70
70
  sig { returns(T.nilable(String)) }
71
71
  attr_reader :repo_contents_path
72
72
 
73
- sig { returns(T::Hash[String, String]) }
73
+ sig { returns(T::Hash[Symbol, Object]) }
74
74
  attr_reader :options
75
75
 
76
76
  CLIENT_NOT_FOUND_ERRORS = T.let(
@@ -135,7 +135,7 @@ module Dependabot
135
135
  source: Dependabot::Source,
136
136
  credentials: T::Array[Dependabot::Credential],
137
137
  repo_contents_path: T.nilable(String),
138
- options: T::Hash[String, String],
138
+ options: T::Hash[Symbol, Object],
139
139
  update_config: T.nilable(Dependabot::Config::UpdateConfig)
140
140
  )
141
141
  .void
@@ -1,4 +1,4 @@
1
- # typed: strict
1
+ # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "excon"
@@ -14,6 +14,7 @@ require "dependabot/utils"
14
14
  require "dependabot/source"
15
15
  require "dependabot/dependency"
16
16
  require "dependabot/credential"
17
+ require "dependabot/clients/github_release"
17
18
  require "dependabot/git_metadata_fetcher"
18
19
  require "dependabot/git_commit_checker/source_details"
19
20
  require "dependabot/git_tag_details"
@@ -485,7 +486,9 @@ module Dependabot
485
486
  def handle_tag_prefix(tags)
486
487
  if dependency_source_details&.ref&.start_with?("tags/")
487
488
  tags = tags.map do |tag|
488
- tag.dup.tap { |t| t.name = "tags/#{tag.name}" }
489
+ duplicated_tag = tag.dup
490
+ duplicated_tag.name = "tags/#{tag.name}"
491
+ duplicated_tag
489
492
  end
490
493
  end
491
494
 
@@ -524,8 +527,11 @@ module Dependabot
524
527
  client = Clients::GithubWithRetries
525
528
  .for_github_dot_com(credentials: credentials)
526
529
 
527
- # TODO: create this method instead of relying on method_missing
528
- T.unsafe(client.compare(T.must(listing_source_repo), ref1, ref2)).status
530
+ comparison = client.compare(T.must(listing_source_repo), ref1, ref2)
531
+ status = T.cast(comparison[:status], Object)
532
+ raise TypeError, "GitHub comparison status must be a string" unless status.is_a?(String)
533
+
534
+ status
529
535
  end
530
536
 
531
537
  sig { params(ref1: String, ref2: String).returns(String) }
@@ -534,9 +540,11 @@ module Dependabot
534
540
  .for_gitlab_dot_com(credentials: credentials)
535
541
 
536
542
  comparison = client.compare(T.must(listing_source_repo), ref1, ref2)
543
+ commits = T.cast(comparison["commits"], Object)
544
+ raise TypeError, "GitLab comparison commits must be an array" unless commits.is_a?(Array)
537
545
 
538
- if T.unsafe(comparison).commits.none? then "behind"
539
- elsif T.unsafe(comparison).compare_same_ref then "identical"
546
+ if commits.empty? then "behind"
547
+ elsif T.cast(comparison["compare_same_ref"], Object) == true then "identical"
540
548
  else
541
549
  "ahead"
542
550
  end
@@ -555,7 +563,13 @@ module Dependabot
555
563
 
556
564
  # Conservatively assume that ref2 is ahead in the equality case, of
557
565
  # if we get an unexpected format (e.g., due to a 404)
558
- if JSON.parse(response.body).fetch("values", ["x"]).none? then "behind"
566
+ comparison = T.cast(JSON.parse(response.body), Object)
567
+ raise TypeError, "Bitbucket comparison must be a hash" unless comparison.is_a?(Hash)
568
+
569
+ values = T.cast(comparison.fetch("values", ["x"]), Object)
570
+ raise TypeError, "Bitbucket comparison values must be an array" unless values.is_a?(Array)
571
+
572
+ if values.empty? then "behind"
559
573
  else
560
574
  "ahead"
561
575
  end
@@ -674,7 +688,9 @@ module Dependabot
674
688
 
675
689
  if dependency_source_details&.ref&.start_with?("tags/")
676
690
  tags = tags.map do |tag|
677
- tag.dup.tap { |t| t.name = "tags/#{tag.name}" }
691
+ duplicated_tag = tag.dup
692
+ duplicated_tag.name = "tags/#{tag.name}"
693
+ duplicated_tag
678
694
  end
679
695
  end
680
696
 
@@ -737,7 +753,7 @@ module Dependabot
737
753
  false
738
754
  end
739
755
 
740
- sig { returns(T::Array[T.untyped]) }
756
+ sig { returns(T::Array[Dependabot::Clients::GithubRelease]) }
741
757
  def github_releases
742
758
  @github_releases ||= T.let(
743
759
  begin
@@ -752,11 +768,17 @@ module Dependabot
752
768
  )
753
769
  # The Octokit RBI types this as non-nil, but it can be nil at runtime
754
770
  # (e.g. an empty/unexpected registry response), so guard against it.
755
- T.unsafe(client.releases(T.must(source).repo, per_page: 100)) || []
771
+ releases = T.let(
772
+ client.releases(T.must(source).repo, per_page: 100),
773
+ T.nilable(T::Array[Sawyer::Resource])
774
+ )
775
+ (releases || []).filter_map do |release|
776
+ Dependabot::Clients::GithubRelease.from_resource(release)
777
+ end
756
778
  rescue Octokit::Error
757
779
  []
758
780
  end,
759
- T.nilable(T::Array[T.untyped])
781
+ T.nilable(T::Array[Dependabot::Clients::GithubRelease])
760
782
  )
761
783
  end
762
784
 
@@ -1,7 +1,8 @@
1
- # typed: strict
1
+ # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "sorbet-runtime"
5
+ require "dependabot/clients/github_release"
5
6
  require "dependabot/clients/github_with_retries"
6
7
  require "dependabot/shared_helpers"
7
8
  require "dependabot/source"
@@ -45,7 +46,8 @@ module Dependabot
45
46
  releases = cached_github_releases
46
47
  unless releases.empty?
47
48
  release = releases.find { |r| r.tag_name == tag_name }
48
- return release.published_at if release&.published_at
49
+ published_at = release&.published_at
50
+ return published_at if published_at
49
51
  end
50
52
 
51
53
  tag_creation_date(tag_name, commit_sha)
@@ -88,7 +90,7 @@ module Dependabot
88
90
 
89
91
  # Fetches and caches GitHub releases for the dependency source.
90
92
  # Returns an empty array for non-GitHub sources.
91
- sig { returns(T::Array[T.untyped]) } # rubocop:disable Sorbet/ForbidTUntyped
93
+ sig { returns(T::Array[Dependabot::Clients::GithubRelease]) }
92
94
  def cached_github_releases
93
95
  @cached_github_releases ||= T.let(
94
96
  begin
@@ -99,7 +101,13 @@ module Dependabot
99
101
  source: T.must(source),
100
102
  credentials: cooldown_credentials
101
103
  )
102
- client.releases(T.must(source).repo, per_page: 100)
104
+ releases = T.let(
105
+ client.releases(T.must(source).repo, per_page: 100),
106
+ T.nilable(T::Array[Sawyer::Resource])
107
+ )
108
+ (releases || []).filter_map do |release|
109
+ Dependabot::Clients::GithubRelease.from_resource(release)
110
+ end
103
111
  else
104
112
  []
105
113
  end
@@ -107,7 +115,7 @@ module Dependabot
107
115
  Dependabot.logger.debug("Error fetching GitHub releases: #{e.message}")
108
116
  []
109
117
  end,
110
- T.nilable(T::Array[T.untyped]) # rubocop:disable Sorbet/ForbidTUntyped
118
+ T.nilable(T::Array[Dependabot::Clients::GithubRelease])
111
119
  )
112
120
  end
113
121
  end
@@ -1,9 +1,11 @@
1
- # typed: strict
1
+ # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "sorbet-runtime"
5
+ require "time"
5
6
 
6
7
  require "dependabot/credential"
8
+ require "dependabot/clients/github_release"
7
9
  require "dependabot/clients/github_with_retries"
8
10
  require "dependabot/clients/gitlab_with_retries"
9
11
  require "dependabot/metadata_finders/base"
@@ -12,40 +14,62 @@ require "dependabot/utils"
12
14
  module Dependabot
13
15
  module MetadataFinders
14
16
  class Base
15
- class GitLabRelease
17
+ class GitLabRelease < T::ImmutableStruct
16
18
  extend T::Sig
17
19
 
18
- sig { returns(String) }
19
- attr_reader :name
20
-
21
- sig { returns(String) }
22
- attr_reader :tag_name
23
-
24
- sig { returns(String) }
25
- attr_reader :body
26
-
27
- sig { returns(String) }
28
- attr_reader :html_url
20
+ const :name, String
21
+ const :tag_name, String
22
+ const :body, T.nilable(String)
23
+ const :html_url, String
24
+ const :authored_at, Time
29
25
 
30
26
  sig do
31
27
  params(
32
- name: String,
33
- tag_name: String,
34
- body: String,
35
- html_url: String
36
- ).void
37
- end
38
- def initialize(name:, tag_name:, body:, html_url:)
39
- @name = name
40
- @tag_name = tag_name
41
- @body = body
42
- @html_url = html_url
28
+ tag: Gitlab::ObjectifiedHash,
29
+ source_url: String
30
+ ).returns(T.nilable(GitLabRelease))
43
31
  end
32
+ def self.from_tag(tag, source_url:)
33
+ name = T.cast(tag["name"], Object)
34
+ release = T.cast(tag["release"], Object)
35
+ commit = T.cast(tag["commit"], Object)
36
+ return unless name.is_a?(String)
37
+ return unless release.is_a?(Gitlab::ObjectifiedHash)
38
+ return unless commit.is_a?(Gitlab::ObjectifiedHash)
39
+
40
+ tag_name = T.cast(release["tag_name"], Object)
41
+ return unless tag_name.is_a?(String)
42
+
43
+ authored_at = time_value(T.cast(commit["authored_date"], Object))
44
+ return unless authored_at
45
+
46
+ description = T.cast(release["description"], Object)
47
+ new(
48
+ name: name,
49
+ tag_name: tag_name,
50
+ body: description.is_a?(String) ? description : nil,
51
+ html_url: "#{source_url}/tags/#{name}",
52
+ authored_at: authored_at
53
+ )
54
+ end
55
+
56
+ sig { params(value: Object).returns(T.nilable(Time)) }
57
+ def self.time_value(value)
58
+ return value if value.is_a?(Time)
59
+ return unless value.is_a?(String)
60
+
61
+ Time.parse(value)
62
+ rescue ArgumentError
63
+ nil
64
+ end
65
+ private_class_method :time_value
44
66
  end
45
67
 
46
68
  class ReleaseFinder
47
69
  extend T::Sig
48
70
 
71
+ ReleaseType = T.type_alias { T.any(Dependabot::Clients::GithubRelease, GitLabRelease) }
72
+
49
73
  sig { returns(Dependabot::Dependency) }
50
74
  attr_reader :dependency
51
75
 
@@ -98,14 +122,13 @@ module Dependabot
98
122
 
99
123
  private
100
124
 
101
- sig { returns(T::Array[T.untyped]) }
125
+ sig { returns(T::Array[ReleaseType]) }
102
126
  def all_dep_releases
103
127
  releases = all_releases
104
128
  dep_prefix = dependency.name.downcase
105
129
 
106
130
  releases_with_dependency_name =
107
131
  releases
108
- .reject { |r| r.tag_name.nil? }
109
132
  .select { |r| r.tag_name.start_with?(dep_prefix) }
110
133
 
111
134
  return releases unless releases_with_dependency_name.any?
@@ -113,12 +136,12 @@ module Dependabot
113
136
  releases_with_dependency_name
114
137
  end
115
138
 
116
- sig { returns(T::Array[T.untyped]) }
139
+ sig { returns(T::Array[ReleaseType]) }
117
140
  def all_releases
118
- @all_releases ||= T.let(fetch_dependency_releases, T.nilable(T::Array[T.untyped]))
141
+ @all_releases ||= T.let(fetch_dependency_releases, T.nilable(T::Array[ReleaseType]))
119
142
  end
120
143
 
121
- sig { returns(T.nilable(T::Array[T.untyped])) }
144
+ sig { returns(T.nilable(T::Array[ReleaseType])) }
122
145
  def relevant_releases
123
146
  releases = releases_since_previous_version
124
147
 
@@ -139,7 +162,7 @@ module Dependabot
139
162
  end
140
163
  end
141
164
 
142
- sig { returns(T.nilable(T::Array[T.untyped])) }
165
+ sig { returns(T.nilable(T::Array[ReleaseType])) }
143
166
  def releases_since_previous_version
144
167
  return [updated_release].compact unless previous_version
145
168
 
@@ -158,21 +181,21 @@ module Dependabot
158
181
  end
159
182
  end
160
183
 
161
- sig { params(releases: T::Array[T.untyped]).returns(T.nilable(T::Array[T.untyped])) }
184
+ sig { params(releases: T::Array[ReleaseType]).returns(T.nilable(T::Array[ReleaseType])) }
162
185
  def filter_releases_using_previous_release(releases)
163
186
  return releases if releases.index(previous_release).nil?
164
187
 
165
188
  releases.first(T.must(releases.index(previous_release)))
166
189
  end
167
190
 
168
- sig { params(releases: T::Array[T.untyped]).returns(T.nilable(T::Array[T.untyped])) }
191
+ sig { params(releases: T::Array[ReleaseType]).returns(T.nilable(T::Array[ReleaseType])) }
169
192
  def filter_releases_using_updated_release(releases)
170
193
  return releases if releases.index(updated_release).nil?
171
194
 
172
195
  releases[releases.index(updated_release)..-1]
173
196
  end
174
197
 
175
- sig { params(releases: T::Array[T.untyped], conservative: T::Boolean).returns(T::Array[T.untyped]) }
198
+ sig { params(releases: T::Array[ReleaseType], conservative: T::Boolean).returns(T::Array[ReleaseType]) }
176
199
  def filter_releases_using_previous_version(releases, conservative:)
177
200
  releases.reject do |release|
178
201
  cleaned_tag = release.tag_name.gsub(/^[^0-9]*/, "")
@@ -193,7 +216,7 @@ module Dependabot
193
216
  end
194
217
  end
195
218
 
196
- sig { params(releases: T::Array[T.untyped], conservative: T::Boolean).returns(T::Array[T.untyped]) }
219
+ sig { params(releases: T::Array[ReleaseType], conservative: T::Boolean).returns(T::Array[ReleaseType]) }
197
220
  def filter_releases_using_updated_version(releases, conservative:)
198
221
  updated_version = version_class.new(new_version)
199
222
 
@@ -216,17 +239,17 @@ module Dependabot
216
239
  end
217
240
  end
218
241
 
219
- sig { returns(T.untyped) }
242
+ sig { returns(T.nilable(ReleaseType)) }
220
243
  def updated_release
221
244
  release_for_version(new_version)
222
245
  end
223
246
 
224
- sig { returns(T.untyped) }
247
+ sig { returns(T.nilable(ReleaseType)) }
225
248
  def previous_release
226
249
  release_for_version(previous_version)
227
250
  end
228
251
 
229
- sig { params(version: T.nilable(String)).returns(T.untyped) }
252
+ sig { params(version: T.nilable(String)).returns(T.nilable(ReleaseType)) }
230
253
  def release_for_version(version)
231
254
  return nil unless version
232
255
 
@@ -236,22 +259,23 @@ module Dependabot
236
259
  all_dep_releases.find { |r| release_regex.match?(r.name.to_s) }
237
260
  end
238
261
 
239
- sig { params(release: T.untyped).returns(String) }
262
+ sig { params(release: ReleaseType).returns(String) }
240
263
  def serialize_release(release)
241
- rel = release
242
- title = "## #{rel.name.to_s == '' ? rel.tag_name : rel.name}\n"
243
- body = if rel.body.to_s.gsub(/\n*\z/m, "") == ""
264
+ name = release.name
265
+ title = "## #{name.to_s == '' ? release.tag_name : name}\n"
266
+ body = if release.body.to_s.gsub(/\n*\z/m, "") == ""
244
267
  "No release notes provided."
245
268
  else
246
- rel.body.gsub(/\n*\z/m, "")
269
+ T.must(release.body).gsub(/\n*\z/m, "")
247
270
  end
248
271
 
249
- release_body_includes_title?(rel) ? body : title + body
272
+ release_body_includes_title?(release) ? body : title + body
250
273
  end
251
274
 
252
- sig { params(release: T.untyped).returns(T::Boolean) }
275
+ sig { params(release: ReleaseType).returns(T::Boolean) }
253
276
  def release_body_includes_title?(release)
254
- title = release.name.to_s == "" ? release.tag_name : release.name
277
+ name = release.name
278
+ title = name.nil? || name.empty? ? release.tag_name : name
255
279
  release.body.to_s.match?(/\A\s*\#*\s*#{Regexp.quote(title)}/m)
256
280
  end
257
281
 
@@ -265,7 +289,7 @@ module Dependabot
265
289
  dependency.version_class
266
290
  end
267
291
 
268
- sig { returns(T::Array[T.untyped]) }
292
+ sig { returns(T::Array[ReleaseType]) }
269
293
  def fetch_dependency_releases
270
294
  return [] unless source
271
295
 
@@ -279,47 +303,45 @@ module Dependabot
279
303
  end
280
304
  end
281
305
 
282
- sig { returns(T::Array[T.untyped]) }
306
+ sig { returns(T::Array[Dependabot::Clients::GithubRelease]) }
283
307
  def fetch_github_releases
284
- releases = github_client.releases(T.must(source).repo, per_page: 100)
285
-
286
- # Remove any releases without a tag name. These are draft releases and
287
- # aren't yet associated with a tag, so shouldn't be used.
288
- releases = releases.reject { |r| T.unsafe(r).tag_name.nil? }
308
+ releases = parsed_github_releases
289
309
 
290
310
  clean_release_names =
291
- releases.map { |r| T.unsafe(r).tag_name.gsub(/^[^0-9\.]*/, "") }
311
+ releases.map { |release| release.tag_name.gsub(/^[^0-9\.]*/, "") }
292
312
 
293
313
  if clean_release_names.all? { |nm| version_class.correct?(nm) }
294
- releases.sort_by do |r|
295
- version_class.new(T.unsafe(r).tag_name.gsub(/^[^0-9\.]*/, ""))
314
+ releases.sort_by do |release|
315
+ version_class.new(release.tag_name.gsub(/^[^0-9\.]*/, ""))
296
316
  end.reverse
297
317
  else
298
- releases.sort_by { |r| T.unsafe(r).id }.reverse
318
+ releases.sort_by { |release| release.id || 0 }.reverse
299
319
  end
300
320
  rescue Octokit::NotFound, Octokit::UnavailableForLegalReasons
301
321
  []
302
322
  end
303
323
 
304
- sig { returns(T::Array[T.untyped]) }
324
+ sig { returns(T::Array[Dependabot::Clients::GithubRelease]) }
325
+ def parsed_github_releases
326
+ resources = T.let(
327
+ github_client.releases(T.must(source).repo, per_page: 100),
328
+ T.nilable(T::Array[Sawyer::Resource])
329
+ )
330
+ releases = (resources || []).filter_map do |release|
331
+ Dependabot::Clients::GithubRelease.from_resource(release)
332
+ end
333
+ releases
334
+ end
335
+
336
+ sig { returns(T::Array[GitLabRelease]) }
305
337
  def fetch_gitlab_releases
306
- releases =
307
- T.unsafe(
308
- gitlab_client
309
- .tags(T.must(source).repo)
310
- )
311
- .select(&:release)
312
- .sort_by { |r| T.unsafe(r).commit.authored_date }
313
- .reverse
314
-
315
- releases.map do |tag|
316
- GitLabRelease.new(
317
- name: T.unsafe(tag).name,
318
- tag_name: T.unsafe(tag).release.tag_name,
319
- body: T.unsafe(tag).release.description,
320
- html_url: "#{T.must(source).url}/tags/#{T.unsafe(tag).name}"
321
- )
338
+ tags = gitlab_client.tags(T.must(source).repo)
339
+ releases = tags.filter_map do |tag|
340
+ next unless tag.is_a?(Gitlab::ObjectifiedHash)
341
+
342
+ GitLabRelease.from_tag(tag, source_url: T.must(source).url)
322
343
  end
344
+ releases.sort_by(&:authored_at).reverse
323
345
  rescue Gitlab::Error::NotFound
324
346
  []
325
347
  end
@@ -359,20 +381,32 @@ module Dependabot
359
381
 
360
382
  sig { returns(T.nilable(String)) }
361
383
  def previous_ref
362
- previous_refs = T.must(dependency.previous_requirements).filter_map do |r|
363
- r.dig(:source, "ref") || r.dig(:source, :ref)
384
+ previous_refs = T.must(dependency.previous_requirements).filter_map do |requirement|
385
+ requirement_ref(requirement)
364
386
  end.uniq
365
387
  previous_refs.first if previous_refs.one?
366
388
  end
367
389
 
368
390
  sig { returns(T.nilable(String)) }
369
391
  def new_ref
370
- new_refs = dependency.requirements.filter_map do |r|
371
- r.dig(:source, "ref") || r.dig(:source, :ref)
392
+ new_refs = dependency.requirements.filter_map do |requirement|
393
+ requirement_ref(requirement)
372
394
  end.uniq
373
395
  new_refs.first if new_refs.one?
374
396
  end
375
397
 
398
+ sig { params(requirement: Dependabot::DependencyRequirement).returns(T.nilable(String)) }
399
+ def requirement_ref(requirement)
400
+ source = requirement.source
401
+ return unless source
402
+
403
+ symbol_ref = source[:ref]
404
+ return symbol_ref if symbol_ref.is_a?(String)
405
+
406
+ string_ref = source["ref"]
407
+ string_ref if string_ref.is_a?(String)
408
+ end
409
+
376
410
  sig { returns(T::Boolean) }
377
411
  def ref_changed?
378
412
  # We could go from multiple previous refs (nil) to a single new ref
@@ -1,4 +1,4 @@
1
- # typed: strict
1
+ # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "sorbet-runtime"
@@ -24,6 +24,15 @@ module Dependabot
24
24
  T.cast(super, Dependabot::Version)
25
25
  end
26
26
 
27
+ sig do
28
+ overridable
29
+ .params(_from_version: String, _to_version: String)
30
+ .returns(T.nilable(String))
31
+ end
32
+ def self.update_type(_from_version, _to_version)
33
+ nil
34
+ end
35
+
27
36
  # Opt-in to Rubygems 4 behavior
28
37
  sig { override.overridable.params(version: VersionParameter).returns(T::Boolean) }
29
38
  def self.correct?(version)
data/lib/dependabot.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Dependabot
5
- VERSION = "0.387.0"
5
+ VERSION = "0.388.0"
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dependabot-common
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.387.0
4
+ version: 0.388.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dependabot
@@ -518,6 +518,7 @@ files:
518
518
  - lib/dependabot/clients/bitbucket.rb
519
519
  - lib/dependabot/clients/bitbucket_with_retries.rb
520
520
  - lib/dependabot/clients/codecommit.rb
521
+ - lib/dependabot/clients/github_release.rb
521
522
  - lib/dependabot/clients/github_with_retries.rb
522
523
  - lib/dependabot/clients/gitlab_with_retries.rb
523
524
  - lib/dependabot/command_helpers.rb
@@ -536,6 +537,7 @@ files:
536
537
  - lib/dependabot/dependency_group.rb
537
538
  - lib/dependabot/dependency_requirement.rb
538
539
  - lib/dependabot/ecosystem.rb
540
+ - lib/dependabot/error_details.rb
539
541
  - lib/dependabot/errors.rb
540
542
  - lib/dependabot/experiments.rb
541
543
  - lib/dependabot/file_fetchers.rb
@@ -623,7 +625,7 @@ licenses:
623
625
  - MIT
624
626
  metadata:
625
627
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
626
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.387.0
628
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.388.0
627
629
  rdoc_options: []
628
630
  require_paths:
629
631
  - lib