dependabot-common 0.391.0 → 0.393.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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/lib/dependabot/clients/azure.rb +42 -22
  3. data/lib/dependabot/clients/bitbucket.rb +52 -26
  4. data/lib/dependabot/clients/bitbucket_with_retries.rb +11 -12
  5. data/lib/dependabot/clients/codecommit.rb +73 -53
  6. data/lib/dependabot/clients/github_resource_parser.rb +70 -0
  7. data/lib/dependabot/clients/github_with_retries.rb +63 -23
  8. data/lib/dependabot/clients/gitlab_with_retries.rb +19 -17
  9. data/lib/dependabot/clients/json_response_parser.rb +103 -0
  10. data/lib/dependabot/command_helpers.rb +89 -13
  11. data/lib/dependabot/credential.rb +12 -3
  12. data/lib/dependabot/dependency_requirement.rb +28 -15
  13. data/lib/dependabot/errors.rb +7 -0
  14. data/lib/dependabot/file_fetchers/base.rb +196 -140
  15. data/lib/dependabot/file_updaters/vendor_updater.rb +8 -2
  16. data/lib/dependabot/git_tag_details.rb +8 -14
  17. data/lib/dependabot/metadata_finders/base/changelog_finder.rb +211 -110
  18. data/lib/dependabot/metadata_finders/base/commit_response_parser.rb +68 -0
  19. data/lib/dependabot/metadata_finders/base/commits_finder.rb +44 -38
  20. data/lib/dependabot/pull_request_creator/azure.rb +1 -1
  21. data/lib/dependabot/pull_request_creator/branch_name_template.rb +1 -1
  22. data/lib/dependabot/pull_request_creator/codecommit.rb +10 -12
  23. data/lib/dependabot/pull_request_creator/github.rb +55 -35
  24. data/lib/dependabot/pull_request_creator/gitlab.rb +37 -11
  25. data/lib/dependabot/pull_request_creator/labeler.rb +65 -15
  26. data/lib/dependabot/pull_request_creator/message_builder/issue_linker.rb +21 -2
  27. data/lib/dependabot/pull_request_creator/message_builder/link_and_mention_sanitizer.rb +53 -13
  28. data/lib/dependabot/pull_request_creator/message_builder/metadata_presenter.rb +20 -13
  29. data/lib/dependabot/pull_request_creator/pr_name_prefixer.rb +179 -78
  30. data/lib/dependabot/pull_request_updater/azure.rb +63 -11
  31. data/lib/dependabot/pull_request_updater/github.rb +44 -34
  32. data/lib/dependabot/pull_request_updater/gitlab.rb +23 -12
  33. data/lib/dependabot/pull_request_updater.rb +3 -3
  34. data/lib/dependabot/shared_helpers.rb +107 -39
  35. data/lib/dependabot/update_checkers/base.rb +9 -5
  36. data/lib/dependabot/update_checkers/version_filters.rb +5 -0
  37. data/lib/dependabot/update_checkers/vulnerability_audit.rb +282 -0
  38. data/lib/dependabot/workspace/git.rb +36 -5
  39. data/lib/dependabot.rb +1 -1
  40. metadata +6 -2
@@ -1,7 +1,6 @@
1
- # typed: strict
1
+ # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
- require "open3"
5
4
  require "timeout"
6
5
  require "sorbet-runtime"
7
6
  require "shellwords"
@@ -23,10 +22,15 @@ module Dependabot
23
22
  T.nilable(T.proc.params(data: String).returns(T::Hash[Symbol, T.anything]))
24
23
  end
25
24
 
25
+ Environment = T.type_alias { T::Hash[String, String] }
26
+ SpawnOptions = T.type_alias { T::Hash[Symbol, T.anything] }
27
+
26
28
  EnvCmdItem = T.type_alias do
27
29
  T.any(
28
30
  String,
29
- T::Hash[T.any(String, Symbol), T.anything]
31
+ T::Array[String],
32
+ Environment,
33
+ SpawnOptions
30
34
  )
31
35
  end
32
36
 
@@ -100,25 +104,31 @@ module Dependabot
100
104
  start_time = Time.now
101
105
 
102
106
  begin
103
- T.unsafe(Open3).popen3(*env_cmd) do |stdin, stdout_io, stderr_io, wait_thr| # rubocop:disable Metrics/BlockLength
107
+ stdout_io, stderr_io, wait_thr = open_process(env_cmd)
108
+ begin
104
109
  pid = wait_thr.pid
105
110
  command_string = command_string_for_logging(env_cmd)
106
111
  log_level = short_git_config_command?(command_string) ? :debug : :info
107
- sanitized_env_cmd = if env_cmd.first.is_a?(Hash)
108
- [SharedHelpers.send(:sanitize_env_for_logging, env_cmd.first), *env_cmd[1..]]
109
- else
110
- env_cmd
111
- end
112
- command_for_log = sanitized_env_cmd.join(" ")
112
+ first_item = env_cmd.first
113
+ sanitized_env_cmd = T.let(
114
+ if first_item.is_a?(Hash)
115
+ environment = extract_environment(env_cmd.dup)
116
+ [T.must(SharedHelpers.sanitize_env_for_logging(environment)), *env_cmd.drop(1)]
117
+ else
118
+ env_cmd
119
+ end,
120
+ T::Array[EnvCmdItem]
121
+ )
122
+ command_for_log = sanitized_env_cmd.map { |item| item.is_a?(Array) ? item.first : item }.join(" ")
113
123
  Dependabot.logger.public_send(log_level, "Started process PID: #{pid} with command: #{command_for_log}")
114
124
 
115
125
  # Write to stdin if input data is provided
116
126
  begin
117
- stdin&.write(stdin_data) if stdin_data
127
+ stdout_io.write(stdin_data) if stdin_data
118
128
  rescue Errno::EPIPE
119
129
  # Process exited before reading stdin - continue to collect output
120
130
  end
121
- stdin&.close
131
+ stdout_io.close_write
122
132
 
123
133
  stdout_io.sync = true
124
134
  stderr_io.sync = true
@@ -190,6 +200,10 @@ module Dependabot
190
200
 
191
201
  status = ProcessStatus.new(wait_thr.value)
192
202
  Dependabot.logger.public_send(log_level, "Process PID: #{pid} completed with status: #{status}")
203
+ ensure
204
+ stdout_io.close unless stdout_io.closed?
205
+ stderr_io.close unless stderr_io.closed?
206
+ wait_thr.join
193
207
  end
194
208
  rescue Timeout::Error => e
195
209
  Dependabot.logger.error("Process PID: #{pid} failed due to timeout: #{e.message}")
@@ -214,6 +228,61 @@ module Dependabot
214
228
  # rubocop:enable Metrics/PerceivedComplexity
215
229
  # rubocop:enable Metrics/CyclomaticComplexity
216
230
 
231
+ sig { params(env_cmd: T::Array[EnvCmdItem]).returns([IO, IO, Process::Waiter]) }
232
+ def self.open_process(env_cmd)
233
+ process_io = T.let(nil, T.nilable(IO))
234
+ stderr_io = T.let(nil, T.nilable(IO))
235
+ stderr_writer = T.let(nil, T.nilable(IO))
236
+ arguments = env_cmd.dup
237
+ environment = extract_environment(arguments)
238
+ options = extract_options(arguments)
239
+ command = command_for_popen(arguments)
240
+
241
+ stderr_io, stderr_writer = IO.pipe
242
+ options[:err] = stderr_writer
243
+ process_io = T.cast(IO.popen(environment, command, "r+", options), IO)
244
+ stderr_writer.close
245
+
246
+ wait_thread = T.cast(Process.detach(process_io.pid), Process::Waiter)
247
+ [process_io, stderr_io, wait_thread]
248
+ rescue StandardError
249
+ process_io.close if process_io && !process_io.closed?
250
+ stderr_io.close if stderr_io && !stderr_io.closed?
251
+ stderr_writer.close if stderr_writer && !stderr_writer.closed?
252
+ raise
253
+ end
254
+ private_class_method :open_process
255
+
256
+ sig { params(arguments: T::Array[EnvCmdItem]).returns(Environment) }
257
+ def self.extract_environment(arguments)
258
+ return {} unless arguments.first.is_a?(Hash)
259
+
260
+ T.cast(arguments.shift, Environment)
261
+ end
262
+ private_class_method :extract_environment
263
+
264
+ sig { params(arguments: T::Array[EnvCmdItem]).returns(SpawnOptions) }
265
+ def self.extract_options(arguments)
266
+ return {} unless arguments.last.is_a?(Hash)
267
+
268
+ T.cast(arguments.pop, SpawnOptions).dup
269
+ end
270
+ private_class_method :extract_options
271
+
272
+ sig do
273
+ params(arguments: T::Array[EnvCmdItem])
274
+ .returns(T.any(String, T::Array[T.any(String, T::Array[String])]))
275
+ end
276
+ def self.command_for_popen(arguments)
277
+ raise ArgumentError, "command must not be empty" if arguments.empty?
278
+
279
+ first_argument = arguments.first
280
+ return first_argument if arguments.one? && first_argument.is_a?(String)
281
+
282
+ T.cast(arguments, T::Array[T.any(String, T::Array[String])])
283
+ end
284
+ private_class_method :command_for_popen
285
+
217
286
  # Terminate a process by PID
218
287
  sig { params(pid: T.nilable(Integer)).void }
219
288
  def self.terminate_process(pid)
@@ -263,7 +332,14 @@ module Dependabot
263
332
 
264
333
  sig { params(env_cmd: T::Array[EnvCmdItem]).returns(T.nilable(String)) }
265
334
  def self.command_string_for_logging(env_cmd)
266
- T.cast(env_cmd.find { |item| item.is_a?(String) }, T.nilable(String))
335
+ command_parts = env_cmd.filter_map do |item|
336
+ case item
337
+ when Array then item.first
338
+ when String then item
339
+ end
340
+ end
341
+
342
+ command_parts.join(" ") unless command_parts.empty?
267
343
  end
268
344
  private_class_method :command_string_for_logging
269
345
 
@@ -9,9 +9,11 @@ module Dependabot
9
9
  extend T::Sig
10
10
  extend Forwardable
11
11
 
12
+ Value = T.type_alias { T.nilable(String) }
13
+
12
14
  def_delegators :@credential, :fetch, :keys, :[]=, :delete, :slice, :values, :entries
13
15
 
14
- sig { params(credential: T::Hash[String, T.any(T::Boolean, String, T::Array[String])]).void }
16
+ sig { params(credential: T::Hash[String, T.nilable(T.any(T::Boolean, String, T::Array[String]))]).void }
15
17
  def initialize(credential)
16
18
  @replaces_base = T.let(credential["replaces-base"] == true, T::Boolean)
17
19
  credential.delete("replaces-base")
@@ -25,7 +27,14 @@ module Dependabot
25
27
  T.nilable(T::Array[String])
26
28
  )
27
29
 
28
- @credential = T.let(T.unsafe(credential), T::Hash[String, String])
30
+ @credential = T.let(
31
+ credential.to_h do |key, value|
32
+ raise TypeError, "credential #{key} must be a string or nil" unless value.nil? || value.is_a?(String)
33
+
34
+ [key, value]
35
+ end,
36
+ T::Hash[String, Value]
37
+ )
29
38
  end
30
39
 
31
40
  sig { returns(T::Boolean) }
@@ -46,7 +55,7 @@ module Dependabot
46
55
  Credential.new(@credential.merge(other.to_h))
47
56
  end
48
57
 
49
- sig { returns(T::Hash[String, String]) }
58
+ sig { returns(T::Hash[String, Value]) }
50
59
  def to_h
51
60
  @credential
52
61
  end
@@ -16,7 +16,7 @@ module Dependabot
16
16
  #
17
17
  # Subclasses Hash so it is a drop-in replacement at call sites (and in
18
18
  # type annotations) that treat requirement entries as
19
- # T::Hash[Symbol, T.untyped], while exposing typed readers for the
19
+ # T::Hash[Symbol, Object], while exposing typed readers for the
20
20
  # well-known keys. New code should prefer the typed readers; hash-style
21
21
  # access remains supported while call sites are migrated gradually.
22
22
  #
@@ -37,15 +37,11 @@ module Dependabot
37
37
  ObjectHash = T.type_alias { T::Hash[T.any(Symbol, String), Object] }
38
38
  Requirement = T.type_alias { T.any(String, Symbol) }
39
39
  Source = T.type_alias { T.any(String, ObjectHash) }
40
- Input = T.type_alias { T.any(DependencyRequirement, ObjectHash) }
40
+ Input = T.type_alias { T::Hash[T.any(Symbol, String), T.anything] }
41
41
 
42
42
  K = type_member { { fixed: Symbol } }
43
- # Hash-style access remains dynamic until ecosystem callers migrate to the
44
- # typed readers. Keeping that compatibility here avoids a flag-day change.
45
- # rubocop:disable Sorbet/ForbidTUntyped
46
- V = type_member { { fixed: T.untyped } }
47
- Elem = type_member { { fixed: [Symbol, T.untyped] } }
48
- # rubocop:enable Sorbet/ForbidTUntyped
43
+ V = type_member { { fixed: Object } }
44
+ Elem = type_member { { fixed: [Symbol, Object] } }
49
45
 
50
46
  # Builds a DependencyRequirement from a requirement hash, symbolising
51
47
  # top-level keys. Accepts both plain hashes and existing
@@ -53,7 +49,12 @@ module Dependabot
53
49
  sig { params(hash: Input).returns(DependencyRequirement) }
54
50
  def self.create(hash)
55
51
  requirement = new
56
- requirement.replace(hash.keys.to_h { |k| [k.to_sym, hash[k]] })
52
+ hash.each do |key, value|
53
+ case value
54
+ when Object then requirement[key.to_sym] = value
55
+ else raise TypeError, "requirement values must inherit Object"
56
+ end
57
+ end
57
58
  requirement
58
59
  end
59
60
 
@@ -62,7 +63,7 @@ module Dependabot
62
63
  # or :unfixable when no valid updated requirement can be generated.
63
64
  sig { returns(T.nilable(Requirement)) }
64
65
  def requirement
65
- value = T.cast(self[:requirement], T.nilable(Object))
66
+ value = self[:requirement]
66
67
  return if value.nil?
67
68
  return value if value.is_a?(String) || value == :unfixable
68
69
 
@@ -82,7 +83,7 @@ module Dependabot
82
83
  # the underlying hash access under sorbet-runtime.
83
84
  sig { returns(T.nilable(T::Array[Group])) }
84
85
  def groups
85
- value = T.cast(self[:groups], T.nilable(Object))
86
+ value = self[:groups]
86
87
  return if value.nil?
87
88
  raise TypeError, "groups must be an array of strings or symbols, or nil" unless value.is_a?(Array)
88
89
 
@@ -101,7 +102,7 @@ module Dependabot
101
102
  # parser or deserialised from a job definition.
102
103
  sig { returns(T.nilable(Source)) }
103
104
  def source
104
- value = T.cast(self[:source], T.nilable(Object))
105
+ value = self[:source]
105
106
  return if value.nil?
106
107
  return value if value.is_a?(String)
107
108
 
@@ -175,6 +176,16 @@ module Dependabot
175
176
  raise TypeError, "metadata #{key} must be a symbol or nil"
176
177
  end
177
178
 
179
+ # Reads a boolean metadata field such as "include_debug_script".
180
+ sig { params(key: String).returns(T.nilable(T::Boolean)) }
181
+ def metadata_boolean(key)
182
+ value = metadata_value(key)
183
+ return if value.nil?
184
+ return value if value == true || value == false
185
+
186
+ raise TypeError, "metadata #{key} must be a boolean or nil"
187
+ end
188
+
178
189
  # Reads a nested metadata hash whose values are all strings, such as
179
190
  # "dependency_set" (`{ group: "my.group", version: "1.4.0" }`). Keys are
180
191
  # symbolised so callers can read them with symbols regardless of how the
@@ -205,12 +216,14 @@ module Dependabot
205
216
  details = metadata
206
217
  return if details.nil?
207
218
 
208
- details[key.to_sym] || details[key]
219
+ return details[key.to_sym] if details.key?(key.to_sym)
220
+
221
+ details[key]
209
222
  end
210
223
 
211
224
  sig { params(key: Symbol).returns(T.nilable(String)) }
212
225
  def optional_string(key)
213
- value = T.cast(self[key], T.nilable(Object))
226
+ value = self[key]
214
227
  return if value.nil?
215
228
  return value if value.is_a?(String)
216
229
 
@@ -219,7 +232,7 @@ module Dependabot
219
232
 
220
233
  sig { params(key: Symbol).returns(T.nilable(ObjectHash)) }
221
234
  def optional_object_hash(key)
222
- value = T.cast(self[key], T.nilable(Object))
235
+ value = self[key]
223
236
  return if value.nil?
224
237
 
225
238
  object_hash(value, key)
@@ -246,6 +246,11 @@ module Dependabot
246
246
  "error-type": "dependency_file_not_resolvable",
247
247
  "error-detail": { message: error.message }
248
248
  }
249
+ when Dependabot::BranchNameFormattingError
250
+ {
251
+ "error-type": "branch_name_formatting_error",
252
+ "error-detail": { message: error.message }
253
+ }
249
254
  when Dependabot::BlockedDependencyVersion
250
255
  {
251
256
  "error-type": "blocked_dependency_version",
@@ -509,6 +514,8 @@ module Dependabot
509
514
 
510
515
  class RefNamespaceConflictError < DependabotError; end
511
516
 
517
+ class BranchNameFormattingError < DependabotError; end
518
+
512
519
  #####################
513
520
  # Repo level errors #
514
521
  #####################