dependabot-npm_and_yarn 0.310.0 → 0.311.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: 9a5cb78b6d6b57d7c62cd5ab60e63db0848277800d48ce32d3a6ea35d06955e9
4
- data.tar.gz: 5180f0822f3bc3588db82562bc9273841651748b92a216aae5e94670e2efe916
3
+ metadata.gz: dd4ac6b246c4c95b8125e2770880b1f729276e4e01990de13e52a1a7fd1f9e6f
4
+ data.tar.gz: 33a4d9f4ea227d1eda55ce264e2a2aa19cf76b04a70b309368bbde7f55c40abe
5
5
  SHA512:
6
- metadata.gz: 53a449f37470153cd80797d0fa6e6a8f48eb2eb3908d89e9b3f2e0f6217ec2eeca704638022525c2b1454919ee439c17221dc707a288628f398d1a43a84fb3d3
7
- data.tar.gz: 60886b10224dcb526953f150f591e814c5333be22747cad39c8b2bee03122b64b252b8846b580d83493de5d9bc0dbd3bb8a80efbb21cc65737eb24dd0da31ab7
6
+ metadata.gz: 3e96d0e963c92f523a3e8c17a0d43461c519b89da2d2a7c06a9b155e0d4d9b4a9e7d7f5df712f1a94b9e00783f146b10ee6ad21c35639e6e1f23e0acec0ee625
7
+ data.tar.gz: c3d66d433a60a3b2e7ed64284be142c9dddd49a564ab0a10f1afe362f7975fdcd806c5aef04d3c8e0fa01d8ce3a882a2d03cd80c086a9ec9fea53634766b2bf5
@@ -35,7 +35,7 @@ module Dependabot
35
35
  .returns(T.nilable(T::Hash[String, T.untyped]))
36
36
  end
37
37
  def details(dependency_name, _requirement, manifest_name)
38
- if Helpers.npm8?(@dependency_file)
38
+ if Helpers.parse_npm8?(@dependency_file)
39
39
  # NOTE: npm 8 sometimes doesn't install workspace dependencies in the
40
40
  # workspace folder so we need to fallback to checking top-level
41
41
  nested_details = parsed.dig("packages", node_modules_path(manifest_name, dependency_name))
@@ -248,22 +248,7 @@ module Dependabot
248
248
 
249
249
  sig { params(top_level_dependencies: T::Array[Dependabot::Dependency]).returns(T::Hash[String, String]) }
250
250
  def run_npm_top_level_updater(top_level_dependencies:)
251
- if npm8?
252
- run_npm8_top_level_updater(top_level_dependencies: top_level_dependencies)
253
- else
254
- T.cast(
255
- SharedHelpers.run_helper_subprocess(
256
- command: NativeHelpers.helper_path,
257
- function: "npm6:update",
258
- args: [
259
- Dir.pwd,
260
- lockfile_basename,
261
- top_level_dependencies.map(&:to_h)
262
- ]
263
- ),
264
- T::Hash[String, String]
265
- )
266
- end
251
+ run_npm8_top_level_updater(top_level_dependencies: top_level_dependencies)
267
252
  end
268
253
 
269
254
  sig { params(top_level_dependencies: T::Array[Dependabot::Dependency]).returns(T::Hash[String, String]) }
@@ -301,18 +286,7 @@ module Dependabot
301
286
  params(sub_dependencies: T::Array[Dependabot::Dependency]).returns(T.nilable(T::Hash[String, String]))
302
287
  end
303
288
  def run_npm_subdependency_updater(sub_dependencies:)
304
- if npm8?
305
- run_npm8_subdependency_updater(sub_dependencies: sub_dependencies)
306
- else
307
- T.cast(
308
- SharedHelpers.run_helper_subprocess(
309
- command: NativeHelpers.helper_path,
310
- function: "npm6:updateSubdependency",
311
- args: [Dir.pwd, lockfile_basename, sub_dependencies.map(&:to_h)]
312
- ),
313
- T.nilable(T::Hash[String, String])
314
- )
315
- end
289
+ run_npm8_subdependency_updater(sub_dependencies: sub_dependencies)
316
290
  end
317
291
 
318
292
  sig { params(sub_dependencies: T::Array[Dependabot::Dependency]).returns(T::Hash[String, String]) }
@@ -894,8 +868,6 @@ module Dependabot
894
868
  .returns(String)
895
869
  end
896
870
  def restore_packages_name(updated_lockfile_content, parsed_updated_lockfile_content)
897
- return updated_lockfile_content unless npm8?
898
-
899
871
  current_name = parsed_updated_lockfile_content.dig("packages", "", "name")
900
872
  original_name = parsed_lockfile.dig("packages", "", "name")
901
873
 
@@ -973,8 +945,6 @@ module Dependabot
973
945
  .returns(String)
974
946
  end
975
947
  def restore_locked_package_dependencies(updated_lockfile_content, parsed_updated_lockfile_content)
976
- return updated_lockfile_content unless npm8?
977
-
978
948
  dependency_names_to_restore = (dependencies.map(&:name) + git_dependencies_to_lock.keys).uniq
979
949
 
980
950
  NpmAndYarn::FileParser.each_dependency(parsed_package_json) do |dependency_name, original_requirement, type|
@@ -1014,14 +984,8 @@ module Dependabot
1014
984
  # updates the lockfile "from" field to the new git commit when we
1015
985
  # run npm install
1016
986
  original_from = %("from": "#{details[:from]}")
1017
- if npm8?
1018
- # NOTE: The `from` syntax has changed in npm 7 to include the dependency name
1019
- npm8_locked_from = %("from": "#{dependency_name}@#{details[:version]}")
1020
- updated_lockfile_content = updated_lockfile_content.gsub(npm8_locked_from, original_from)
1021
- else
1022
- npm6_locked_from = %("from": "#{details[:version]}")
1023
- updated_lockfile_content = updated_lockfile_content.gsub(npm6_locked_from, original_from)
1024
- end
987
+ npm8_locked_from = %("from": "#{dependency_name}@#{details[:version]}")
988
+ updated_lockfile_content = updated_lockfile_content.gsub(npm8_locked_from, original_from)
1025
989
  end
1026
990
 
1027
991
  updated_lockfile_content
@@ -1103,16 +1067,6 @@ module Dependabot
1103
1067
  npmrc_content.match?(/^package-lock\s*=\s*false/)
1104
1068
  end
1105
1069
 
1106
- sig { returns(T::Boolean) }
1107
- def npm8?
1108
- return T.must(@npm8) if defined?(@npm8)
1109
-
1110
- @npm8 ||= T.let(
1111
- Dependabot::NpmAndYarn::Helpers.npm8?(lockfile),
1112
- T.nilable(T::Boolean)
1113
- )
1114
- end
1115
-
1116
1070
  sig { params(package_name: String).returns(String) }
1117
1071
  def sanitize_package_name(package_name)
1118
1072
  package_name.gsub("%2f", "/").gsub("%2F", "/")
@@ -1,4 +1,4 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "dependabot/npm_and_yarn/helpers"
@@ -9,24 +9,47 @@ require "dependabot/shared_helpers"
9
9
  module Dependabot
10
10
  module NpmAndYarn
11
11
  class FileUpdater < Dependabot::FileUpdaters::Base
12
+ # rubocop:disable Metrics/ClassLength
12
13
  class PnpmLockfileUpdater
14
+ extend T::Sig
15
+
13
16
  require_relative "npmrc_builder"
14
17
  require_relative "package_json_updater"
15
18
 
19
+ sig do
20
+ params(
21
+ dependencies: T::Array[Dependabot::Dependency],
22
+ dependency_files: T::Array[Dependabot::DependencyFile],
23
+ repo_contents_path: T.nilable(String),
24
+ credentials: T::Array[Dependabot::Credential]
25
+ ).void
26
+ end
16
27
  def initialize(dependencies:, dependency_files:, repo_contents_path:, credentials:)
17
28
  @dependencies = dependencies
18
29
  @dependency_files = dependency_files
19
30
  @repo_contents_path = repo_contents_path
20
31
  @credentials = credentials
21
- @error_handler = PnpmErrorHandler.new(
22
- dependencies: dependencies,
23
- dependency_files: dependency_files
32
+ @error_handler = T.let(
33
+ PnpmErrorHandler.new(
34
+ dependencies: dependencies,
35
+ dependency_files: dependency_files
36
+ ),
37
+ PnpmErrorHandler
24
38
  )
25
39
  end
26
40
 
41
+ sig do
42
+ params(
43
+ pnpm_lock: Dependabot::DependencyFile,
44
+ updated_pnpm_workspace_content: T.nilable(T::Hash[String, T.nilable(String)])
45
+ ).returns(String)
46
+ end
27
47
  def updated_pnpm_lock_content(pnpm_lock, updated_pnpm_workspace_content: nil)
28
- @updated_pnpm_lock_content ||= {}
29
- return @updated_pnpm_lock_content[pnpm_lock.name] if @updated_pnpm_lock_content[pnpm_lock.name]
48
+ @updated_pnpm_lock_content ||= T.let(
49
+ {},
50
+ T.nilable(T::Hash[String, String])
51
+ )
52
+ return T.must(@updated_pnpm_lock_content[pnpm_lock.name]) if @updated_pnpm_lock_content[pnpm_lock.name]
30
53
 
31
54
  new_content = run_pnpm_update(
32
55
  pnpm_lock: pnpm_lock,
@@ -39,10 +62,19 @@ module Dependabot
39
62
 
40
63
  private
41
64
 
65
+ sig { returns(T::Array[Dependabot::Dependency]) }
42
66
  attr_reader :dependencies
67
+
68
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
43
69
  attr_reader :dependency_files
70
+
71
+ sig { returns(T.nilable(String)) }
44
72
  attr_reader :repo_contents_path
73
+
74
+ sig { returns(T::Array[Dependabot::Credential]) }
45
75
  attr_reader :credentials
76
+
77
+ sig { returns(PnpmErrorHandler) }
46
78
  attr_reader :error_handler
47
79
 
48
80
  IRRESOLVABLE_PACKAGE = "ERR_PNPM_NO_MATCHING_VERSION"
@@ -103,6 +135,13 @@ module Dependabot
103
135
  # Peer dependencies configuration error
104
136
  ERR_PNPM_PEER_DEP_ISSUES = /ERR_PNPM_PEER_DEP_ISSUES/
105
137
 
138
+ sig do
139
+ params(
140
+ pnpm_lock: Dependabot::DependencyFile,
141
+ updated_pnpm_workspace_content: T.nilable(T::Hash[String, T.nilable(String)])
142
+ )
143
+ .returns(String)
144
+ end
106
145
  def run_pnpm_update(pnpm_lock:, updated_pnpm_workspace_content: nil)
107
146
  SharedHelpers.in_a_temporary_repo_directory(base_dir, repo_contents_path) do
108
147
  File.write(".npmrc", npmrc_content(pnpm_lock))
@@ -122,6 +161,7 @@ module Dependabot
122
161
  end
123
162
  end
124
163
 
164
+ sig { returns(T.nilable(String)) }
125
165
  def run_pnpm_update_packages
126
166
  dependency_updates = dependencies.map do |d|
127
167
  "#{d.name}@#{d.version}"
@@ -133,18 +173,24 @@ module Dependabot
133
173
  )
134
174
  end
135
175
 
176
+ sig { returns(T.nilable(String)) }
136
177
  def run_pnpm_install
137
178
  Helpers.run_pnpm_command(
138
179
  "install --lockfile-only"
139
180
  )
140
181
  end
141
182
 
183
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
142
184
  def workspace_files
143
- @workspace_files ||= dependency_files.select { |f| f.name.end_with?("pnpm-workspace.yaml") }
185
+ @workspace_files ||= T.let(
186
+ dependency_files.select { |f| f.name.end_with?("pnpm-workspace.yaml") },
187
+ T.nilable(T::Array[Dependabot::DependencyFile])
188
+ )
144
189
  end
145
190
 
191
+ sig { params(lockfile: Dependabot::DependencyFile).returns(T::Array[Dependabot::Dependency]) }
146
192
  def lockfile_dependencies(lockfile)
147
- @lockfile_dependencies ||= {}
193
+ @lockfile_dependencies ||= T.let({}, T.nilable(T::Hash[String, T::Array[Dependabot::Dependency]]))
148
194
  @lockfile_dependencies[lockfile.name] ||=
149
195
  NpmAndYarn::FileParser.new(
150
196
  dependency_files: [lockfile, *package_files, *workspace_files],
@@ -157,6 +203,13 @@ module Dependabot
157
203
  # rubocop:disable Metrics/PerceivedComplexity
158
204
  # rubocop:disable Metrics/MethodLength
159
205
  # rubocop:disable Metrics/CyclomaticComplexity
206
+ sig do
207
+ params(
208
+ error: SharedHelpers::HelperSubprocessFailed,
209
+ pnpm_lock: Dependabot::DependencyFile
210
+ )
211
+ .returns(T.noreturn)
212
+ end
160
213
  def handle_pnpm_lock_updater_error(error, pnpm_lock)
161
214
  error_message = error.message
162
215
 
@@ -165,15 +218,15 @@ module Dependabot
165
218
  end
166
219
 
167
220
  if error_message.match?(UNREACHABLE_GIT)
168
- url = error_message.match(UNREACHABLE_GIT).named_captures.fetch("url").gsub("git+ssh://git@", "https://").delete_suffix(".git")
221
+ url = error_message.match(UNREACHABLE_GIT)&.named_captures&.fetch("url")&.gsub("git+ssh://git@", "https://")&.delete_suffix(".git")
169
222
 
170
- raise Dependabot::GitDependenciesNotReachable, url
223
+ raise Dependabot::GitDependenciesNotReachable, T.must(url)
171
224
  end
172
225
 
173
226
  if error_message.match?(UNREACHABLE_GIT_V8)
174
- url = error_message.match(UNREACHABLE_GIT_V8).named_captures.fetch("url").gsub("codeload.", "")
227
+ url = error_message.match(UNREACHABLE_GIT_V8)&.named_captures&.fetch("url")&.gsub("codeload.", "")
175
228
 
176
- raise Dependabot::GitDependenciesNotReachable, url
229
+ raise Dependabot::GitDependenciesNotReachable, T.must(url)
177
230
  end
178
231
 
179
232
  [FORBIDDEN_PACKAGE, MISSING_PACKAGE, UNAUTHORIZED_PACKAGE, ERR_PNPM_FETCH_401,
@@ -181,14 +234,13 @@ module Dependabot
181
234
  .each do |regexp|
182
235
  next unless error_message.match?(regexp)
183
236
 
184
- dependency_url = error_message.match(regexp).named_captures["dependency_url"]
237
+ dependency_url = T.must(error_message.match(regexp)&.named_captures&.[]("dependency_url"))
185
238
  raise_package_access_error(error_message, dependency_url, pnpm_lock)
186
239
  end
187
240
 
188
241
  # TO-DO : subclassifcation of ERR_PNPM_TARBALL_INTEGRITY errors
189
242
  if error_message.match?(ERR_PNPM_TARBALL_INTEGRITY)
190
243
  dependency_names = dependencies.map(&:name).join(", ")
191
-
192
244
  msg = "Error (ERR_PNPM_TARBALL_INTEGRITY) while resolving \"#{dependency_names}\"."
193
245
  Dependabot.logger.warn(error_message)
194
246
  raise Dependabot::DependencyFileNotResolvable, msg
@@ -197,20 +249,17 @@ module Dependabot
197
249
  # TO-DO : investigate "packageManager" allowed regex
198
250
  if error_message.match?(INVALID_PACKAGE_SPEC)
199
251
  dependency_names = dependencies.map(&:name).join(", ")
200
-
201
252
  msg = "Invalid package manager specification in package.json while resolving \"#{dependency_names}\"."
202
253
  raise Dependabot::DependencyFileNotResolvable, msg
203
254
  end
204
255
 
205
256
  if error_message.match?(ERR_PNPM_META_FETCH_FAIL)
206
-
207
257
  msg = error_message.split(ERR_PNPM_META_FETCH_FAIL).last
208
258
  raise Dependabot::DependencyFileNotResolvable, msg
209
259
  end
210
260
 
211
261
  if error_message.match?(ERR_PNPM_WORKSPACE_PKG_NOT_FOUND)
212
262
  dependency_names = dependencies.map(&:name).join(", ")
213
-
214
263
  msg = "No package named \"#{dependency_names}\" present in workspace."
215
264
  Dependabot.logger.warn(error_message)
216
265
  raise Dependabot::DependencyFileNotResolvable, msg
@@ -223,8 +272,8 @@ module Dependabot
223
272
  end
224
273
 
225
274
  if error_message.match?(ERR_PNPM_LINKED_PKG_DIR_NOT_FOUND)
226
- dir = error_message.match(ERR_PNPM_LINKED_PKG_DIR_NOT_FOUND).named_captures.fetch("dir")
227
- msg = "Could not find linked package installation directory \"#{dir.split('/').last}\""
275
+ dir = error_message.match(ERR_PNPM_LINKED_PKG_DIR_NOT_FOUND)&.named_captures&.fetch("dir")
276
+ msg = "Could not find linked package installation directory \"#{dir&.split('/')&.last}\""
228
277
  raise Dependabot::DependencyFileNotResolvable, msg
229
278
  end
230
279
 
@@ -246,13 +295,11 @@ module Dependabot
246
295
 
247
296
  if error_message.match?(ERR_PNPM_PEER_DEP_ISSUES)
248
297
  msg = "Missing or invalid configuration while installing peer dependencies."
249
-
250
298
  Dependabot.logger.warn(error_message)
251
299
  raise Dependabot::DependencyFileNotResolvable, msg
252
300
  end
253
301
 
254
302
  raise_patch_dependency_error(error_message) if error_message.match?(ERR_PNPM_PATCH_NOT_APPLIED)
255
-
256
303
  raise_unsupported_engine_error(error_message, pnpm_lock) if error_message.match?(ERR_PNPM_UNSUPPORTED_ENGINE)
257
304
 
258
305
  if error_message.match?(ERR_INVALID_THIS) && error_message.match?(URL_SEARCH_PARAMS)
@@ -262,8 +309,7 @@ module Dependabot
262
309
  end
263
310
 
264
311
  if error_message.match?(ERR_PNPM_UNSUPPORTED_PLATFORM)
265
- raise_unsupported_platform_error(error_message,
266
- pnpm_lock)
312
+ raise_unsupported_platform_error(error_message, pnpm_lock)
267
313
  end
268
314
 
269
315
  error_handler.handle_pnpm_error(error)
@@ -275,6 +321,7 @@ module Dependabot
275
321
  # rubocop:enable Metrics/MethodLength
276
322
  # rubocop:enable Metrics/CyclomaticComplexity
277
323
 
324
+ sig { params(error_message: String, pnpm_lock: Dependabot::DependencyFile).returns(T.noreturn) }
278
325
  def raise_resolvability_error(error_message, pnpm_lock)
279
326
  dependency_names = dependencies.map(&:name).join(", ")
280
327
  msg = "Error whilst updating #{dependency_names} in " \
@@ -282,6 +329,7 @@ module Dependabot
282
329
  raise Dependabot::DependencyFileNotResolvable, msg
283
330
  end
284
331
 
332
+ sig { params(error_message: String).returns(T.noreturn) }
285
333
  def raise_patch_dependency_error(error_message)
286
334
  dependency_names = dependencies.map(&:name).join(", ")
287
335
  msg = "Error while updating \"#{dependency_names}\" in " \
@@ -290,21 +338,50 @@ module Dependabot
290
338
  raise Dependabot::DependencyFileNotResolvable, msg
291
339
  end
292
340
 
341
+ sig do
342
+ params(
343
+ error_message: String,
344
+ _pnpm_lock: Dependabot::DependencyFile
345
+ ).returns(T.nilable(T.noreturn))
346
+ end
293
347
  def raise_unsupported_engine_error(error_message, _pnpm_lock)
294
- unless error_message.match(PACAKGE_MANAGER) &&
295
- error_message.match(VERSION_REQUIREMENT)
296
- return
348
+ match_pkg_mgr = error_message.match(PACAKGE_MANAGER)
349
+ match_version = error_message.match(VERSION_REQUIREMENT)
350
+
351
+ unless match_pkg_mgr && match_version &&
352
+ match_pkg_mgr.named_captures && match_version.named_captures
353
+ return nil
297
354
  end
298
355
 
299
- package_manager = error_message.match(PACAKGE_MANAGER).named_captures["pkg_mgr"]
300
- supported_version = error_message.match(VERSION_REQUIREMENT).named_captures["supported_ver"]
301
- detected_version = error_message.match(VERSION_REQUIREMENT).named_captures["detected_ver"]
356
+ captures_pkg_mgr = match_pkg_mgr.named_captures
357
+ captures_version = match_version.named_captures
358
+
359
+ pkg_mgr = captures_pkg_mgr["pkg_mgr"]
360
+ supported_ver = captures_version["supported_ver"]
361
+ detected_ver = captures_version["detected_ver"]
362
+
363
+ if pkg_mgr && supported_ver && detected_ver
364
+ raise Dependabot::ToolVersionNotSupported.new(
365
+ pkg_mgr,
366
+ supported_ver,
367
+ detected_ver
368
+ )
369
+ end
302
370
 
303
- raise Dependabot::ToolVersionNotSupported.new(package_manager, supported_version, detected_version)
371
+ nil
304
372
  end
305
373
 
374
+ sig do
375
+ params(
376
+ error_message: String,
377
+ dependency_url: String,
378
+ pnpm_lock: Dependabot::DependencyFile
379
+ )
380
+ .returns(T.noreturn)
381
+ end
306
382
  def raise_package_access_error(error_message, dependency_url, pnpm_lock)
307
- package_name = RegistryParser.new(resolved_url: dependency_url, credentials: credentials).dependency_name
383
+ package_name = RegistryParser.new(resolved_url: dependency_url,
384
+ credentials: credentials).dependency_name
308
385
  missing_dep = lockfile_dependencies(pnpm_lock)
309
386
  .find { |dep| dep.name == package_name }
310
387
  raise DependencyNotFound, package_name unless missing_dep
@@ -318,6 +395,7 @@ module Dependabot
318
395
  raise PrivateSourceAuthenticationFailure, reg
319
396
  end
320
397
 
398
+ sig { void }
321
399
  def write_final_package_json_files
322
400
  package_files.each do |file|
323
401
  path = file.name
@@ -326,57 +404,88 @@ module Dependabot
326
404
  end
327
405
  end
328
406
 
407
+ sig do
408
+ params(
409
+ error_message: String,
410
+ _pnpm_lock: Dependabot::DependencyFile
411
+ )
412
+ .returns(T.nilable(T.noreturn))
413
+ end
329
414
  def raise_unsupported_platform_error(error_message, _pnpm_lock)
330
- unless error_message.match(PLATFORM_PACAKGE_DEP) &&
331
- error_message.match(PLATFORM_VERSION_REQUIREMENT)
332
- return
415
+ match_dep = error_message.match(PLATFORM_PACAKGE_DEP)
416
+ match_version = error_message.match(PLATFORM_VERSION_REQUIREMENT)
417
+
418
+ unless match_dep && match_version &&
419
+ match_dep.named_captures && match_version.named_captures
420
+ return nil
333
421
  end
334
422
 
335
- supported_version = error_message.match(PLATFORM_VERSION_REQUIREMENT)
336
- .named_captures["supported_ver"]
337
- .then { sanitize_message(_1) }
338
- detected_version = error_message.match(PLATFORM_VERSION_REQUIREMENT)
339
- .named_captures["detected_ver"]
340
- .then { sanitize_message(_1) }
423
+ captures_version = match_version.named_captures
341
424
 
342
- Dependabot.logger.warn(error_message)
343
- raise Dependabot::ToolVersionNotSupported.new(PLATFORM_PACAKGE_MANAGER, supported_version, detected_version)
425
+ supported_ver = captures_version["supported_ver"]
426
+ detected_ver = captures_version["detected_ver"]
427
+
428
+ if supported_ver && detected_ver
429
+ supported_version = sanitize_message(supported_ver)
430
+ detected_version = sanitize_message(detected_ver)
431
+
432
+ Dependabot.logger.warn(error_message)
433
+ raise Dependabot::ToolVersionNotSupported.new(
434
+ PLATFORM_PACAKGE_MANAGER,
435
+ supported_version,
436
+ detected_version
437
+ )
438
+ end
439
+
440
+ nil
344
441
  end
345
442
 
443
+ sig { params(pnpm_lock: Dependabot::DependencyFile).returns(String) }
346
444
  def npmrc_content(pnpm_lock)
347
445
  NpmrcBuilder.new(
348
- credentials: credentials,
446
+ credentials: T.unsafe(credentials),
349
447
  dependency_files: dependency_files,
350
448
  dependencies: lockfile_dependencies(pnpm_lock)
351
449
  ).npmrc_content
352
450
  end
353
451
 
452
+ sig { params(file: Dependabot::DependencyFile).returns(String) }
354
453
  def updated_package_json_content(file)
355
- @updated_package_json_content ||= {}
454
+ @updated_package_json_content ||= T.let({}, T.nilable(T::Hash[String, String]))
356
455
  @updated_package_json_content[file.name] ||=
357
- PackageJsonUpdater.new(
358
- package_json: file,
359
- dependencies: dependencies
360
- ).updated_package_json.content
456
+ T.must(
457
+ PackageJsonUpdater.new(
458
+ package_json: file,
459
+ dependencies: dependencies
460
+ ).updated_package_json.content
461
+ )
361
462
  end
362
463
 
464
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
363
465
  def package_files
364
- @package_files ||= dependency_files.select { |f| f.name.end_with?("package.json") }
466
+ @package_files ||= T.let(
467
+ dependency_files.select { |f| f.name.end_with?("package.json") },
468
+ T.nilable(T::Array[Dependabot::DependencyFile])
469
+ )
365
470
  end
366
471
 
472
+ sig { returns(String) }
367
473
  def base_dir
368
- dependency_files.first.directory
474
+ T.must(dependency_files.first).directory
369
475
  end
370
476
 
477
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
371
478
  def npmrc_file
372
479
  dependency_files.find { |f| f.name == ".npmrc" }
373
480
  end
374
481
 
482
+ sig { params(message: String).returns(String) }
375
483
  def sanitize_message(message)
376
484
  message.gsub(/"|\[|\]|\}|\{/, "")
377
485
  end
378
486
  end
379
487
  end
488
+ # rubocop:enable Metrics/ClassLength
380
489
 
381
490
  class PnpmErrorHandler
382
491
  extend T::Sig
@@ -400,7 +509,8 @@ module Dependabot
400
509
  params(
401
510
  dependencies: T::Array[Dependabot::Dependency],
402
511
  dependency_files: T::Array[Dependabot::DependencyFile]
403
- ).void
512
+ )
513
+ .void
404
514
  end
405
515
  def initialize(dependencies:, dependency_files:)
406
516
  @dependencies = dependencies
@@ -43,39 +43,17 @@ module Dependabot
43
43
  # corepack supported package managers
44
44
  SUPPORTED_COREPACK_PACKAGE_MANAGERS = %w(npm yarn pnpm).freeze
45
45
 
46
- # Determines the npm version depends to the feature flag
47
- # If the feature flag is enabled, we are going to use the minimum version npm 8
48
- # Otherwise, we are going to use old versionining npm 6
49
46
  sig { params(lockfile: T.nilable(DependencyFile)).returns(Integer) }
50
47
  def self.npm_version_numeric(lockfile)
51
- fallback_version_npm8 = Dependabot::Experiments.enabled?(:npm_fallback_version_above_v6)
48
+ detected_npm_version = detect_npm_version(lockfile)
52
49
 
53
- return npm_version_numeric_npm8_or_higher(lockfile) if fallback_version_npm8
50
+ return NPM_DEFAULT_VERSION if detected_npm_version.nil? || detected_npm_version == NPM_V6
54
51
 
55
- npm_version_numeric_npm6_or_higher(lockfile)
52
+ detected_npm_version
56
53
  end
57
54
 
58
- sig { params(lockfile: T.nilable(DependencyFile)).returns(Integer) }
59
- def self.npm_version_numeric_npm6_or_higher(lockfile)
60
- lockfile_content = lockfile&.content
61
-
62
- if lockfile_content.nil? ||
63
- lockfile_content.strip.empty? ||
64
- JSON.parse(lockfile_content)["lockfileVersion"].to_i >= 2
65
- return NPM_V8
66
- end
67
-
68
- NPM_V6
69
- rescue JSON::ParserError
70
- NPM_V6
71
- end
72
-
73
- # Determines the npm version based on the lockfile version
74
- # - NPM 7 uses lockfileVersion 2
75
- # - NPM 8 uses lockfileVersion 2
76
- # - NPM 9 uses lockfileVersion 3
77
- sig { params(lockfile: T.nilable(DependencyFile)).returns(Integer) }
78
- def self.npm_version_numeric_npm8_or_higher(lockfile)
55
+ sig { params(lockfile: T.nilable(DependencyFile)).returns(T.nilable(Integer)) }
56
+ def self.detect_npm_version(lockfile)
79
57
  lockfile_content = lockfile&.content
80
58
 
81
59
  # Return default NPM version if there's no lockfile or it's empty
@@ -94,40 +72,13 @@ module Dependabot
94
72
  # Update needed to support npm 9+ based on lockfile version.
95
73
  return NPM_V8 if lockfile_version >= 2
96
74
 
97
- NPM_DEFAULT_VERSION
75
+ NPM_V6 if lockfile_version >= 1
76
+ # Return nil if can't capture
98
77
  rescue JSON::ParserError
99
78
  NPM_DEFAULT_VERSION # Fallback to default npm version if parsing fails
100
79
  end
101
80
 
102
- # rubocop:disable Metrics/PerceivedComplexity
103
- sig { params(lockfile: T.nilable(DependencyFile)).returns(Integer) }
104
- def self.npm_version_numeric_latest(lockfile)
105
- lockfile_content = lockfile&.content
106
-
107
- # Return npm 10 as the default if the lockfile is missing or empty
108
- return NPM_V10 if lockfile_content.nil? || lockfile_content.strip.empty?
109
-
110
- # Parse the lockfile content to extract the `lockfileVersion`
111
- parsed_lockfile = JSON.parse(lockfile_content)
112
- lockfile_version = parsed_lockfile["lockfileVersion"]&.to_i
113
-
114
- # Determine the appropriate npm version based on `lockfileVersion`
115
- if lockfile_version.nil?
116
- NPM_V10 # Use npm 10 if `lockfileVersion` is missing or nil
117
- elsif lockfile_version >= 3
118
- NPM_V10 # Use npm 10 for lockfileVersion 3 or higher
119
- elsif lockfile_version >= 2
120
- NPM_V8 # Use npm 8 for lockfileVersion 2
121
- elsif lockfile_version >= 1
122
- # Use npm 8 if the fallback version flag is enabled, otherwise use npm 6
123
- Dependabot::Experiments.enabled?(:npm_fallback_version_above_v6) ? NPM_V8 : NPM_V6
124
- else
125
- NPM_V10 # Default to npm 10 for unexpected or unsupported versions
126
- end
127
- rescue JSON::ParserError
128
- NPM_V8 # Fallback to npm 8 if the lockfile content cannot be parsed
129
- end
130
- # rubocop:enable Metrics/PerceivedComplexity
81
+ private_class_method :detect_npm_version
131
82
 
132
83
  sig { params(yarn_lock: T.nilable(DependencyFile)).returns(Integer) }
133
84
  def self.yarn_version_numeric(yarn_lock)
@@ -179,10 +130,12 @@ module Dependabot
179
130
  end
180
131
 
181
132
  sig { params(package_lock: T.nilable(DependencyFile)).returns(T::Boolean) }
182
- def self.npm8?(package_lock)
133
+ def self.parse_npm8?(package_lock)
183
134
  return true unless package_lock&.content
184
135
 
185
- npm_version_numeric(package_lock) == NPM_V8
136
+ detected_npm = detect_npm_version(package_lock)
137
+ # For conversion reading properly from npm 6 lockfile we need to check if detected version is npm 6
138
+ detected_npm.nil? || detected_npm != NPM_V6
186
139
  end
187
140
 
188
141
  sig { params(yarn_lock: T.nilable(DependencyFile)).returns(T::Boolean) }
@@ -129,15 +129,20 @@ module Dependabot
129
129
 
130
130
  sig do
131
131
  params(
132
- details: T.nilable(T.any(String, T::Hash[String, T.untyped]))
133
- )
134
- .returns(T.nilable(String))
132
+ details: T.nilable(T.any(String, T::Array[String], T::Hash[String, String]))
133
+ ).returns(T.nilable(String))
135
134
  end
136
135
  def get_url(details)
136
+ return unless details
137
+
137
138
  url =
138
139
  case details
139
140
  when String then details
140
141
  when Hash then details.fetch("url", nil)
142
+ when Array
143
+ # Try to find the first valid URL string, and if not, return the first string (even if it isn't a URL)
144
+ details.find { |d| d.is_a?(String) && d.match?(%r{^[\w.-]+/[\w.-]+$}) } ||
145
+ details.find { |d| d.is_a?(String) }
141
146
  end
142
147
  return url unless url&.match?(%r{^[\w.-]+/[\w.-]+$})
143
148
 
@@ -215,6 +220,9 @@ module Dependabot
215
220
  new_source&.fetch(:url)
216
221
  end
217
222
 
223
+ # spaces must be escaped in base URL
224
+ registry_url = registry_url.gsub(" ", "%20")
225
+
218
226
  # NPM registries expect slashes to be escaped
219
227
  escaped_dependency_name = dependency.name.gsub("/", "%2F")
220
228
  "#{registry_url}/#{escaped_dependency_name}"
@@ -1,4 +1,4 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "dependabot/dependency"
@@ -12,11 +12,42 @@ require "dependabot/npm_and_yarn/update_checker"
12
12
  require "dependabot/npm_and_yarn/update_checker/dependency_files_builder"
13
13
  require "dependabot/npm_and_yarn/version"
14
14
  require "dependabot/shared_helpers"
15
+ require "sorbet-runtime"
15
16
 
16
17
  module Dependabot
17
18
  module NpmAndYarn
18
19
  class UpdateChecker
19
20
  class SubdependencyVersionResolver
21
+ extend T::Sig
22
+
23
+ sig { returns(Dependency) }
24
+ attr_reader :dependency
25
+
26
+ sig { returns(T::Array[Dependabot::Credential]) }
27
+ attr_reader :credentials
28
+
29
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
30
+ attr_reader :dependency_files
31
+
32
+ sig { returns(T::Array[String]) }
33
+ attr_reader :ignored_versions
34
+
35
+ sig { returns(T.nilable(T.any(String, Gem::Version))) }
36
+ attr_reader :latest_allowable_version
37
+
38
+ sig { returns(T.nilable(String)) }
39
+ attr_reader :repo_contents_path
40
+
41
+ sig do
42
+ params(
43
+ dependency: Dependency,
44
+ credentials: T::Array[Dependabot::Credential],
45
+ dependency_files: T::Array[Dependabot::DependencyFile],
46
+ ignored_versions: T::Array[String],
47
+ latest_allowable_version: T.nilable(T.any(String, Gem::Version)),
48
+ repo_contents_path: T.nilable(String)
49
+ ).void
50
+ end
20
51
  def initialize(dependency:, credentials:, dependency_files:,
21
52
  ignored_versions:, latest_allowable_version:, repo_contents_path:)
22
53
  @dependency = dependency
@@ -27,11 +58,12 @@ module Dependabot
27
58
  @repo_contents_path = repo_contents_path
28
59
  end
29
60
 
61
+ sig { returns(T.nilable(T.any(String, Gem::Version))) }
30
62
  def latest_resolvable_version
31
63
  raise "Not a subdependency!" if dependency.requirements.any?
32
64
  return if bundled_dependency?
33
65
 
34
- base_dir = dependency_files.first.directory
66
+ base_dir = T.must(dependency_files.first).directory
35
67
  SharedHelpers.in_a_temporary_repo_directory(base_dir, repo_contents_path) do
36
68
  dependency_files_builder.write_temporary_dependency_files
37
69
 
@@ -53,13 +85,7 @@ module Dependabot
53
85
 
54
86
  private
55
87
 
56
- attr_reader :dependency
57
- attr_reader :credentials
58
- attr_reader :dependency_files
59
- attr_reader :ignored_versions
60
- attr_reader :latest_allowable_version
61
- attr_reader :repo_contents_path
62
-
88
+ sig { params(lockfile: Dependabot::DependencyFile).returns(String) }
63
89
  def update_subdependency_in_lockfile(lockfile)
64
90
  lockfile_name = Pathname.new(lockfile.name).basename.to_s
65
91
  path = Pathname.new(lockfile.name).dirname.to_s
@@ -72,7 +98,7 @@ module Dependabot
72
98
  run_pnpm_updater(path, lockfile_name)
73
99
  elsif lockfile.name.end_with?("bun.lock")
74
100
  run_bun_updater(path, lockfile_name)
75
- elsif !Helpers.npm8?(lockfile)
101
+ elsif !Helpers.parse_npm8?(lockfile)
76
102
  run_npm6_updater(path, lockfile_name)
77
103
  else
78
104
  run_npm_updater(path, lockfile_name)
@@ -81,6 +107,7 @@ module Dependabot
81
107
  updated_files.fetch(lockfile_name)
82
108
  end
83
109
 
110
+ sig { params(updated_lockfiles: T::Array[Dependabot::DependencyFile]).returns(T.nilable(Gem::Version)) }
84
111
  def version_from_updated_lockfiles(updated_lockfiles)
85
112
  updated_files = dependency_files -
86
113
  dependency_files_builder.lockfiles +
@@ -96,13 +123,17 @@ module Dependabot
96
123
  version_class.new(updated_version)
97
124
  end
98
125
 
126
+ sig { params(path: String, lockfile_name: String).returns(T::Hash[String, String]) }
99
127
  def run_yarn_updater(path, lockfile_name)
100
128
  SharedHelpers.with_git_configured(credentials: credentials) do
101
129
  Dir.chdir(path) do
102
- SharedHelpers.run_helper_subprocess(
103
- command: NativeHelpers.helper_path,
104
- function: "yarn:updateSubdependency",
105
- args: [Dir.pwd, lockfile_name, [dependency.to_h]]
130
+ T.cast(
131
+ SharedHelpers.run_helper_subprocess(
132
+ command: NativeHelpers.helper_path,
133
+ function: "yarn:updateSubdependency",
134
+ args: [Dir.pwd, lockfile_name, [dependency.to_h]]
135
+ ),
136
+ T::Hash[String, String]
106
137
  )
107
138
  end
108
139
  end
@@ -121,6 +152,7 @@ module Dependabot
121
152
  retry
122
153
  end
123
154
 
155
+ sig { params(path: String, lockfile_name: String).returns(T::Hash[String, String]) }
124
156
  def run_yarn_berry_updater(path, lockfile_name)
125
157
  SharedHelpers.with_git_configured(credentials: credentials) do
126
158
  Dir.chdir(path) do
@@ -133,6 +165,7 @@ module Dependabot
133
165
  end
134
166
  end
135
167
 
168
+ sig { params(path: String, lockfile_name: String).returns(T::Hash[String, String]) }
136
169
  def run_pnpm_updater(path, lockfile_name)
137
170
  SharedHelpers.with_git_configured(credentials: credentials) do
138
171
  Dir.chdir(path) do
@@ -145,6 +178,7 @@ module Dependabot
145
178
  end
146
179
  end
147
180
 
181
+ sig { params(path: String, lockfile_name: String).returns(T::Hash[String, String]) }
148
182
  def run_npm_updater(path, lockfile_name)
149
183
  SharedHelpers.with_git_configured(credentials: credentials) do
150
184
  Dir.chdir(path) do
@@ -155,6 +189,7 @@ module Dependabot
155
189
  end
156
190
  end
157
191
 
192
+ sig { params(path: String, lockfile_name: String).returns(T::Hash[String, String]) }
158
193
  def run_bun_updater(path, lockfile_name)
159
194
  SharedHelpers.with_git_configured(credentials: credentials) do
160
195
  Dir.chdir(path) do
@@ -167,6 +202,7 @@ module Dependabot
167
202
  end
168
203
  end
169
204
 
205
+ sig { params(path: String, lockfile_name: String).returns(T::Hash[String, String]) }
170
206
  def run_npm6_updater(path, lockfile_name)
171
207
  SharedHelpers.with_git_configured(credentials: credentials) do
172
208
  Dir.chdir(path) do
@@ -179,53 +215,46 @@ module Dependabot
179
215
  end
180
216
  end
181
217
 
218
+ sig { returns(T.class_of(Dependabot::Version)) }
182
219
  def version_class
183
220
  dependency.version_class
184
221
  end
185
222
 
223
+ sig { returns(Dependabot::Dependency) }
186
224
  def updated_dependency
187
225
  Dependabot::Dependency.new(
188
226
  name: dependency.name,
189
- version: latest_allowable_version,
227
+ version: T.cast(latest_allowable_version, T.nilable(T.any(String, Dependabot::Version))),
190
228
  previous_version: dependency.version,
191
229
  requirements: [],
192
230
  package_manager: dependency.package_manager
193
231
  )
194
232
  end
195
233
 
234
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
196
235
  def filtered_lockfiles
197
- @filtered_lockfiles ||=
236
+ @filtered_lockfiles ||= T.let(
198
237
  SubDependencyFilesFilterer.new(
199
238
  dependency_files: dependency_files,
200
239
  updated_dependencies: [updated_dependency]
201
- ).files_requiring_update
240
+ ).files_requiring_update,
241
+ T.nilable(T::Array[Dependabot::DependencyFile])
242
+ )
202
243
  end
203
244
 
245
+ sig { returns(Dependabot::NpmAndYarn::UpdateChecker::DependencyFilesBuilder) }
204
246
  def dependency_files_builder
205
- @dependency_files_builder ||=
247
+ @dependency_files_builder ||= T.let(
206
248
  DependencyFilesBuilder.new(
207
249
  dependency: dependency,
208
250
  dependency_files: dependency_files,
209
251
  credentials: credentials
210
- )
211
- end
212
-
213
- # TODO: We should try and fix this by updating the parent that's not
214
- # bundled. For this case: `chokidar > fsevents > node-pre-gyp > tar` we
215
- # would need to update `fsevents`
216
- #
217
- # We shouldn't update bundled sub-dependencies as they have been bundled
218
- # into the release at an exact version by a parent using
219
- # `bundledDependencies`.
220
- #
221
- # For example, fsevents < 2 bundles node-pre-gyp meaning all it's
222
- # sub-dependencies get bundled into the release tarball at publish time
223
- # so you always get the same sub-dependency versions if you re-install a
224
- # specific version of fsevents.
225
- #
226
- # Updating the sub-dependency by deleting the entry works but it gets
227
- # removed from the bundled set of dependencies and moved top level
228
- # resulting in a bunch of package duplication which is pretty confusing.
252
+ ),
253
+ T.nilable(Dependabot::NpmAndYarn::UpdateChecker::DependencyFilesBuilder)
254
+ )
255
+ end
256
+
257
+ sig { returns(T::Boolean) }
229
258
  def bundled_dependency?
230
259
  dependency.subdependency_metadata
231
260
  &.any? { |h| h.fetch(:npm_bundled, false) } ||
@@ -825,7 +825,7 @@ module Dependabot
825
825
  f.name == [path, "package-lock.json"].join("/").sub(%r{\A.?\/}, "")
826
826
  end
827
827
 
828
- return run_npm8_checker(version: version) if Dependabot::NpmAndYarn::Helpers.npm8?(package_lock)
828
+ return run_npm8_checker(version: version) if Dependabot::NpmAndYarn::Helpers.parse_npm8?(package_lock)
829
829
 
830
830
  SharedHelpers.run_helper_subprocess(
831
831
  command: NativeHelpers.helper_path,
@@ -43,7 +43,7 @@ module Dependabot
43
43
  requirements_update_strategy: nil, dependency_group: nil,
44
44
  update_cooldown: nil, options: {})
45
45
  @latest_version = T.let(nil, T.nilable(T.any(String, Gem::Version)))
46
- @latest_resolvable_version = T.let(nil, T.nilable(T.any(String, Gem::Version)))
46
+ @latest_resolvable_version = T.let(nil, T.nilable(T.any(String, Dependabot::Version)))
47
47
  @updated_requirements = T.let(nil, T.nilable(T::Array[T::Hash[Symbol, T.untyped]]))
48
48
  @vulnerability_audit = T.let(nil, T.nilable(T::Hash[String, T.untyped]))
49
49
  @vulnerable_versions = T.let(nil, T.nilable(T::Array[T.any(String, Gem::Version)]))
@@ -88,17 +88,23 @@ module Dependabot
88
88
  end
89
89
  end
90
90
 
91
- sig { override.returns(T.nilable(T.any(String, Dependabot::Version))) }
91
+ sig { override.returns(T.nilable(T.any(String, Gem::Version))) }
92
92
  def latest_resolvable_version
93
93
  return unless latest_version
94
94
 
95
95
  @latest_resolvable_version ||=
96
96
  if dependency.top_level?
97
- version_resolver.latest_resolvable_version
97
+ T.cast(
98
+ version_resolver.latest_resolvable_version,
99
+ T.nilable(T.any(String, Dependabot::Version))
100
+ )
98
101
  else
99
102
  # If the dependency is indirect its version is constrained by the
100
103
  # requirements placed on it by dependencies lower down the tree
101
- subdependency_version_resolver.latest_resolvable_version
104
+ T.cast(
105
+ subdependency_version_resolver.latest_resolvable_version,
106
+ T.nilable(T.any(String, Dependabot::Version))
107
+ )
102
108
  end
103
109
  end
104
110
 
@@ -133,7 +139,11 @@ module Dependabot
133
139
 
134
140
  sig { override.returns(T.nilable(T.any(String, Dependabot::Version))) }
135
141
  def latest_resolvable_version_with_no_unlock
136
- return latest_resolvable_version unless dependency.top_level?
142
+ unless dependency.top_level?
143
+ # TODO: We should use `Dependabot::Version` everywhere
144
+ return T.cast(latest_resolvable_version,
145
+ T.nilable(T.any(String, Dependabot::Version)))
146
+ end
137
147
 
138
148
  return latest_resolvable_version_with_no_unlock_for_git_dependency if git_dependency?
139
149
 
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dependabot-npm_and_yarn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.310.0
4
+ version: 0.311.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dependabot
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-04-24 00:00:00.000000000 Z
10
+ date: 2025-05-01 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: dependabot-common
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 0.310.0
18
+ version: 0.311.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.310.0
25
+ version: 0.311.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: debug
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -223,16 +223,16 @@ dependencies:
223
223
  name: webrick
224
224
  requirement: !ruby/object:Gem::Requirement
225
225
  requirements:
226
- - - ">="
226
+ - - "~>"
227
227
  - !ruby/object:Gem::Version
228
- version: '1.7'
228
+ version: '1.9'
229
229
  type: :development
230
230
  prerelease: false
231
231
  version_requirements: !ruby/object:Gem::Requirement
232
232
  requirements:
233
- - - ">="
233
+ - - "~>"
234
234
  - !ruby/object:Gem::Version
235
- version: '1.7'
235
+ version: '1.9'
236
236
  description: Dependabot-NPM_And_Yarn provides support for bumping Javascript (npm
237
237
  and yarn) libraries via Dependabot. If you want support for multiple package managers,
238
238
  you probably want the meta-gem dependabot-omnibus.
@@ -356,7 +356,7 @@ licenses:
356
356
  - MIT
357
357
  metadata:
358
358
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
359
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.310.0
359
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.311.0
360
360
  rdoc_options: []
361
361
  require_paths:
362
362
  - lib