dependabot-npm_and_yarn 0.236.0 → 0.237.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,6 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "json"
5
+ require "sorbet-runtime"
5
6
  require "dependabot/experiments"
6
7
  require "dependabot/logger"
7
8
  require "dependabot/file_fetchers"
@@ -14,6 +15,9 @@ require "dependabot/npm_and_yarn/file_parser/lockfile_parser"
14
15
  module Dependabot
15
16
  module NpmAndYarn
16
17
  class FileFetcher < Dependabot::FileFetchers::Base # rubocop:disable Metrics/ClassLength
18
+ extend T::Sig
19
+ extend T::Helpers
20
+
17
21
  require_relative "file_fetcher/path_dependency_builder"
18
22
 
19
23
  # Npm always prefixes file paths in the lockfile "version" with "file:"
@@ -56,7 +60,7 @@ module Dependabot
56
60
  def ecosystem_versions
57
61
  package_managers = {}
58
62
 
59
- package_managers["npm"] = Helpers.npm_version_numeric(package_lock.content) if package_lock
63
+ package_managers["npm"] = npm_version if package_lock
60
64
  package_managers["yarn"] = yarn_version if yarn_version
61
65
  package_managers["pnpm"] = pnpm_version if pnpm_version
62
66
  package_managers["shrinkwrap"] = 1 if shrinkwrap
@@ -67,12 +71,7 @@ module Dependabot
67
71
  }
68
72
  end
69
73
 
70
- private
71
-
72
- def recurse_submodules_when_cloning?
73
- true
74
- end
75
-
74
+ sig { override.returns(T::Array[DependencyFile]) }
76
75
  def fetch_files
77
76
  fetched_files = []
78
77
  fetched_files << package_json
@@ -86,6 +85,12 @@ module Dependabot
86
85
  fetched_files.uniq
87
86
  end
88
87
 
88
+ private
89
+
90
+ def recurse_submodules_when_cloning?
91
+ true
92
+ end
93
+
89
94
  def npm_files
90
95
  fetched_npm_files = []
91
96
  fetched_npm_files << package_lock if package_lock
@@ -165,10 +170,14 @@ module Dependabot
165
170
  @inferred_npmrc = nil
166
171
  end
167
172
 
173
+ def npm_version
174
+ Helpers.npm_version_numeric(package_lock.content)
175
+ end
176
+
168
177
  def yarn_version
169
178
  return @yarn_version if defined?(@yarn_version)
170
179
 
171
- @yarn_version = package_manager.locked_version("yarn") || guess_yarn_version
180
+ @yarn_version = package_manager.requested_version("yarn") || guess_yarn_version
172
181
  end
173
182
 
174
183
  def guess_yarn_version
@@ -180,13 +189,19 @@ module Dependabot
180
189
  def pnpm_version
181
190
  return @pnpm_version if defined?(@pnpm_version)
182
191
 
183
- @pnpm_version = package_manager.locked_version("pnpm") || guess_pnpm_version
192
+ version = package_manager.requested_version("pnpm") || guess_pnpm_version
193
+
194
+ if version && Version.new(version.to_s) < Version.new("7")
195
+ raise ToolVersionNotSupported.new("PNPM", version.to_s, "7.*, 8.*")
196
+ end
197
+
198
+ @pnpm_version = version
184
199
  end
185
200
 
186
201
  def guess_pnpm_version
187
202
  return unless pnpm_lock
188
203
 
189
- Helpers.pnpm_major_version
204
+ Helpers.pnpm_version_numeric(pnpm_lock)
190
205
  end
191
206
 
192
207
  def package_manager
@@ -7,7 +7,7 @@ require "dependabot/npm_and_yarn/helpers"
7
7
 
8
8
  module Dependabot
9
9
  module NpmAndYarn
10
- class FileParser
10
+ class FileParser < Dependabot::FileParsers::Base
11
11
  class JsonLock
12
12
  def initialize(dependency_file)
13
13
  @dependency_file = dependency_file
@@ -7,7 +7,7 @@ require "dependabot/npm_and_yarn/helpers"
7
7
 
8
8
  module Dependabot
9
9
  module NpmAndYarn
10
- class FileParser
10
+ class FileParser < Dependabot::FileParsers::Base
11
11
  class LockfileParser
12
12
  require "dependabot/npm_and_yarn/file_parser/yarn_lock"
13
13
  require "dependabot/npm_and_yarn/file_parser/pnpm_lock"
@@ -5,7 +5,7 @@ require "dependabot/errors"
5
5
 
6
6
  module Dependabot
7
7
  module NpmAndYarn
8
- class FileParser
8
+ class FileParser < Dependabot::FileParsers::Base
9
9
  class PnpmLock
10
10
  def initialize(dependency_file)
11
11
  @dependency_file = dependency_file
@@ -7,7 +7,7 @@ require "dependabot/npm_and_yarn/native_helpers"
7
7
 
8
8
  module Dependabot
9
9
  module NpmAndYarn
10
- class FileParser
10
+ class FileParser < Dependabot::FileParsers::Base
11
11
  class YarnLock
12
12
  def initialize(dependency_file)
13
13
  @dependency_file = dependency_file
@@ -11,6 +11,7 @@ require "dependabot/npm_and_yarn/helpers"
11
11
  require "dependabot/npm_and_yarn/native_helpers"
12
12
  require "dependabot/npm_and_yarn/version"
13
13
  require "dependabot/npm_and_yarn/requirement"
14
+ require "dependabot/npm_and_yarn/registry_parser"
14
15
  require "dependabot/git_metadata_fetcher"
15
16
  require "dependabot/git_commit_checker"
16
17
  require "dependabot/errors"
@@ -34,6 +35,15 @@ module Dependabot
34
35
  )?$
35
36
  }ix
36
37
 
38
+ def self.each_dependency(json)
39
+ DEPENDENCY_TYPES.each do |type|
40
+ deps = json[type] || {}
41
+ deps.each do |name, requirement|
42
+ yield name, requirement, type
43
+ end
44
+ end
45
+ end
46
+
37
47
  def parse
38
48
  dependency_set = DependencySet.new
39
49
  dependency_set += manifest_dependencies
@@ -64,22 +74,24 @@ module Dependabot
64
74
  dependency_set = DependencySet.new
65
75
 
66
76
  package_files.each do |file|
77
+ json = JSON.parse(file.content)
78
+
67
79
  # TODO: Currently, Dependabot can't handle flat dependency files
68
80
  # (and will error at the FileUpdater stage, because the
69
81
  # UpdateChecker doesn't take account of flat resolution).
70
- next if JSON.parse(file.content)["flat"]
71
-
72
- DEPENDENCY_TYPES.each do |type|
73
- deps = JSON.parse(file.content)[type] || {}
74
- deps.each do |name, requirement|
75
- next unless requirement.is_a?(String)
76
-
77
- requirement = "*" if requirement == ""
78
- dep = build_dependency(
79
- file: file, type: type, name: name, requirement: requirement
80
- )
81
- dependency_set << dep if dep
82
- end
82
+ next if json["flat"]
83
+
84
+ self.class.each_dependency(json) do |name, requirement, type|
85
+ next unless requirement.is_a?(String)
86
+
87
+ # Skip dependencies using Yarn workspace cross-references as requirements
88
+ next if requirement.start_with?("workspace:")
89
+
90
+ requirement = "*" if requirement == ""
91
+ dep = build_dependency(
92
+ file: file, type: type, name: name, requirement: requirement
93
+ )
94
+ dependency_set << dep if dep
83
95
  end
84
96
  end
85
97
 
@@ -258,7 +270,10 @@ module Dependabot
258
270
  return unless resolved_url.start_with?("http")
259
271
  return if resolved_url.match?(/(?<!pkg\.)github/)
260
272
 
261
- registry_source_for(resolved_url, name)
273
+ RegistryParser.new(
274
+ resolved_url: resolved_url,
275
+ credentials: credentials
276
+ ).registry_source_for(name)
262
277
  end
263
278
 
264
279
  def requirement_for(requirement)
@@ -291,54 +306,6 @@ module Dependabot
291
306
  }
292
307
  end
293
308
 
294
- def registry_source_for(resolved_url, name)
295
- url =
296
- if resolved_url.include?("/~/")
297
- # Gemfury format
298
- resolved_url.split("/~/").first
299
- elsif resolved_url.include?("/#{name}/-/#{name}")
300
- # MyGet / Bintray format
301
- resolved_url.split("/#{name}/-/#{name}").first
302
- .gsub("dl.bintray.com//", "api.bintray.com/npm/").
303
- # GitLab format
304
- gsub(%r{\/projects\/\d+}, "")
305
- elsif resolved_url.include?("/#{name}/-/#{name.split('/').last}")
306
- # Sonatype Nexus / Artifactory JFrog format
307
- resolved_url.split("/#{name}/-/#{name.split('/').last}").first
308
- elsif (cred_url = url_for_relevant_cred(resolved_url)) then cred_url
309
- else
310
- resolved_url.split("/")[0..2].join("/")
311
- end
312
-
313
- { type: "registry", url: url }
314
- end
315
-
316
- def url_for_relevant_cred(resolved_url)
317
- resolved_url_host = URI(resolved_url).host
318
-
319
- credential_matching_url =
320
- credentials
321
- .select { |cred| cred["type"] == "npm_registry" }
322
- .sort_by { |cred| cred["registry"].length }
323
- .find do |details|
324
- next true if resolved_url_host == details["registry"]
325
-
326
- uri = if details["registry"]&.include?("://")
327
- URI(details["registry"])
328
- else
329
- URI("https://#{details['registry']}")
330
- end
331
- resolved_url_host == uri.host && resolved_url.include?(details["registry"])
332
- end
333
-
334
- return unless credential_matching_url
335
-
336
- # Trim the resolved URL so that it ends at the same point as the
337
- # credential registry
338
- reg = credential_matching_url["registry"]
339
- resolved_url.gsub(/#{Regexp.quote(reg)}.*/, "") + reg
340
- end
341
-
342
309
  def support_package_files
343
310
  @support_package_files ||= sub_package_files.select(&:support_file?)
344
311
  end
@@ -13,7 +13,7 @@ require "dependabot/shared_helpers"
13
13
  # rubocop:disable Metrics/ClassLength
14
14
  module Dependabot
15
15
  module NpmAndYarn
16
- class FileUpdater
16
+ class FileUpdater < Dependabot::FileUpdaters::Base
17
17
  class NpmLockfileUpdater
18
18
  require_relative "npmrc_builder"
19
19
  require_relative "package_json_updater"
@@ -562,13 +562,11 @@ module Dependabot
562
562
  return content if git_dependencies_to_lock.empty?
563
563
 
564
564
  json = JSON.parse(content)
565
- NpmAndYarn::FileParser::DEPENDENCY_TYPES.each do |type|
566
- json.fetch(type, {}).each do |nm, _|
567
- updated_version = git_dependencies_to_lock.dig(nm, :version)
568
- next unless updated_version
565
+ NpmAndYarn::FileParser.each_dependency(json) do |nm, _, type|
566
+ updated_version = git_dependencies_to_lock.dig(nm, :version)
567
+ next unless updated_version
569
568
 
570
- json[type][nm] = git_dependencies_to_lock[nm][:version]
571
- end
569
+ json[type][nm] = git_dependencies_to_lock[nm][:version]
572
570
  end
573
571
 
574
572
  indent = detect_indentation(content)
@@ -605,12 +603,10 @@ module Dependabot
605
603
  def lock_deps_with_latest_reqs(content)
606
604
  json = JSON.parse(content)
607
605
 
608
- NpmAndYarn::FileParser::DEPENDENCY_TYPES.each do |type|
609
- json.fetch(type, {}).each do |nm, requirement|
610
- next unless requirement == "latest"
606
+ NpmAndYarn::FileParser.each_dependency(json) do |nm, requirement, type|
607
+ next unless requirement == "latest"
611
608
 
612
- json[type][nm] = "*"
613
- end
609
+ json[type][nm] = "*"
614
610
  end
615
611
 
616
612
  indent = detect_indentation(content)
@@ -721,17 +717,15 @@ module Dependabot
721
717
 
722
718
  dependency_names_to_restore = (dependencies.map(&:name) + git_dependencies_to_lock.keys).uniq
723
719
 
724
- NpmAndYarn::FileParser::DEPENDENCY_TYPES.each do |type|
725
- parsed_package_json.fetch(type, {}).each do |dependency_name, original_requirement|
726
- next unless dependency_names_to_restore.include?(dependency_name)
720
+ NpmAndYarn::FileParser.each_dependency(parsed_package_json) do |dependency_name, original_requirement, type|
721
+ next unless dependency_names_to_restore.include?(dependency_name)
727
722
 
728
- locked_requirement = parsed_updated_lockfile_content.dig("packages", "", type, dependency_name)
729
- next unless locked_requirement
723
+ locked_requirement = parsed_updated_lockfile_content.dig("packages", "", type, dependency_name)
724
+ next unless locked_requirement
730
725
 
731
- locked_req = %("#{dependency_name}": "#{locked_requirement}")
732
- original_req = %("#{dependency_name}": "#{original_requirement}")
733
- updated_lockfile_content = updated_lockfile_content.gsub(locked_req, original_req)
734
- end
726
+ locked_req = %("#{dependency_name}": "#{locked_requirement}")
727
+ original_req = %("#{dependency_name}": "#{original_requirement}")
728
+ updated_lockfile_content = updated_lockfile_content.gsub(locked_req, original_req)
735
729
  end
736
730
 
737
731
  updated_lockfile_content
@@ -5,7 +5,7 @@ require "dependabot/npm_and_yarn/file_updater"
5
5
 
6
6
  module Dependabot
7
7
  module NpmAndYarn
8
- class FileUpdater
8
+ class FileUpdater < Dependabot::FileUpdaters::Base
9
9
  # Build a .npmrc file from the lockfile content, credentials, and any
10
10
  # committed .npmrc
11
11
  # We should refactor this to use UpdateChecker::RegistryFinder
@@ -140,7 +140,10 @@ module Dependabot
140
140
  @dependency_urls = dependencies.map do |dependency|
141
141
  UpdateChecker::RegistryFinder.new(
142
142
  dependency: dependency,
143
- credentials: credentials
143
+ credentials: credentials,
144
+ npmrc_file: npmrc_file,
145
+ yarnrc_file: yarnrc_file,
146
+ yarnrc_yml_file: yarnrc_yml_file
144
147
  ).dependency_url
145
148
  end
146
149
  return @dependency_urls
@@ -309,6 +312,11 @@ module Dependabot
309
312
  .find { |f| f.name.end_with?(".yarnrc") }
310
313
  end
311
314
 
315
+ def yarnrc_yml_file
316
+ @yarnrc_yml_file ||= dependency_files
317
+ .find { |f| f.name.end_with?(".yarnrc.yml") }
318
+ end
319
+
312
320
  def yarn_lock
313
321
  @yarn_lock ||= dependency_files.find { |f| f.name == "yarn.lock" }
314
322
  end
@@ -6,7 +6,7 @@ require "dependabot/npm_and_yarn/file_parser"
6
6
 
7
7
  module Dependabot
8
8
  module NpmAndYarn
9
- class FileUpdater
9
+ class FileUpdater < Dependabot::FileUpdaters::Base
10
10
  class PackageJsonPreparer
11
11
  def initialize(package_json_content:)
12
12
  @package_json_content = package_json_content
@@ -72,14 +72,12 @@ module Dependabot
72
72
 
73
73
  @git_ssh_requirements_to_swap = []
74
74
 
75
- NpmAndYarn::FileParser::DEPENDENCY_TYPES.each do |t|
76
- JSON.parse(package_json_content).fetch(t, {}).each do |_, req|
77
- next unless req.is_a?(String)
78
- next unless req.start_with?("git+ssh:")
75
+ NpmAndYarn::FileParser.each_dependency(JSON.parse(package_json_content)) do |_, req, _t|
76
+ next unless req.is_a?(String)
77
+ next unless req.start_with?("git+ssh:")
79
78
 
80
- req = req.split("#").first
81
- @git_ssh_requirements_to_swap << req
82
- end
79
+ req = req.split("#").first
80
+ @git_ssh_requirements_to_swap << req
83
81
  end
84
82
 
85
83
  @git_ssh_requirements_to_swap
@@ -5,7 +5,7 @@ require "dependabot/npm_and_yarn/file_updater"
5
5
 
6
6
  module Dependabot
7
7
  module NpmAndYarn
8
- class FileUpdater
8
+ class FileUpdater < Dependabot::FileUpdaters::Base
9
9
  class PackageJsonUpdater
10
10
  def initialize(package_json:, dependencies:)
11
11
  @package_json = package_json
@@ -3,11 +3,12 @@
3
3
 
4
4
  require "dependabot/npm_and_yarn/helpers"
5
5
  require "dependabot/npm_and_yarn/update_checker/registry_finder"
6
+ require "dependabot/npm_and_yarn/registry_parser"
6
7
  require "dependabot/shared_helpers"
7
8
 
8
9
  module Dependabot
9
10
  module NpmAndYarn
10
- class FileUpdater
11
+ class FileUpdater < Dependabot::FileUpdaters::Base
11
12
  class PnpmLockfileUpdater
12
13
  require_relative "npmrc_builder"
13
14
  require_relative "package_json_updater"
@@ -35,7 +36,8 @@ module Dependabot
35
36
 
36
37
  IRRESOLVABLE_PACKAGE = "ERR_PNPM_NO_MATCHING_VERSION"
37
38
  INVALID_REQUIREMENT = "ERR_PNPM_SPEC_NOT_SUPPORTED_BY_ANY_RESOLVER"
38
- MISSING_PACKAGE = /(?<package_req>.*?) is not in the npm registry, or you have no permission to fetch it/
39
+ UNREACHABLE_GIT = %r{ERR_PNPM_FETCH_404[ [^:print:]]+GET (?<url>https://codeload\.github\.com/[^/]+/[^/]+)/}
40
+ MISSING_PACKAGE = /ERR_PNPM_FETCH_404[ [^:print:]]+GET (?<dependency_url>.*): Not Found - 404/
39
41
 
40
42
  def run_pnpm_update(pnpm_lock:)
41
43
  SharedHelpers.in_a_temporary_repo_directory(base_dir, repo_contents_path) do
@@ -87,11 +89,18 @@ module Dependabot
87
89
  raise_resolvability_error(error_message, pnpm_lock)
88
90
  end
89
91
 
92
+ if error_message.match?(UNREACHABLE_GIT)
93
+ dependency_url = error_message.match(UNREACHABLE_GIT).named_captures.fetch("url")
94
+
95
+ raise Dependabot::GitDependenciesNotReachable, dependency_url
96
+ end
97
+
90
98
  raise unless error_message.match?(MISSING_PACKAGE)
91
99
 
92
- package_name = error_message.match(MISSING_PACKAGE)
93
- .named_captures["package_req"]
94
- .split(/(?<=\w)\@/).first
100
+ dependency_url = error_message.match(MISSING_PACKAGE)
101
+ .named_captures["dependency_url"]
102
+
103
+ package_name = RegistryParser.new(resolved_url: dependency_url, credentials: credentials).dependency_name
95
104
  raise_missing_package_error(package_name, error_message, pnpm_lock)
96
105
  end
97
106
 
@@ -14,7 +14,7 @@ require "dependabot/errors"
14
14
  # rubocop:disable Metrics/ClassLength
15
15
  module Dependabot
16
16
  module NpmAndYarn
17
- class FileUpdater
17
+ class FileUpdater < Dependabot::FileUpdaters::Base
18
18
  class YarnLockfileUpdater
19
19
  require_relative "npmrc_builder"
20
20
  require_relative "package_json_updater"
@@ -25,6 +25,16 @@ module Dependabot
25
25
  end
26
26
  end
27
27
 
28
+ # Mapping from lockfile versions to PNPM versions is at
29
+ # https://github.com/pnpm/spec/tree/274ff02de23376ad59773a9f25ecfedd03a41f64/lockfile, but simplify it for now.
30
+ def self.pnpm_version_numeric(pnpm_lock)
31
+ if pnpm_lockfile_version(pnpm_lock).to_f >= 5.4
32
+ 8
33
+ else
34
+ 6
35
+ end
36
+ end
37
+
28
38
  def self.fetch_yarnrc_yml_value(key, default_value)
29
39
  if File.exist?(".yarnrc.yml") && (yarnrc = YAML.load_file(".yarnrc.yml"))
30
40
  yarnrc.fetch(key, default_value)
@@ -44,20 +54,11 @@ module Dependabot
44
54
  @yarn_major_version ||= fetch_yarn_major_version
45
55
  end
46
56
 
47
- def self.pnpm_major_version
48
- @pnpm_major_version ||= fetch_pnpm_major_version
49
- end
50
-
51
57
  def self.fetch_yarn_major_version
52
58
  output = SharedHelpers.run_shell_command("yarn --version")
53
59
  Version.new(output).major
54
60
  end
55
61
 
56
- def self.fetch_pnpm_major_version
57
- output = SharedHelpers.run_shell_command("pnpm --version")
58
- Version.new(output).major
59
- end
60
-
61
62
  def self.yarn_zero_install?
62
63
  File.exist?(".pnp.cjs")
63
64
  end
@@ -122,6 +123,10 @@ module Dependabot
122
123
  SharedHelpers.run_shell_command(command, fingerprint: fingerprint)
123
124
  end
124
125
 
126
+ def self.pnpm_lockfile_version(pnpm_lock)
127
+ pnpm_lock.content.match(/^lockfileVersion: ['"]?(?<version>[\d.]+)/)[:version]
128
+ end
129
+
125
130
  def self.dependencies_with_all_versions_metadata(dependency_set)
126
131
  dependency_set.dependencies.map do |dependency|
127
132
  dependency.metadata[:all_versions] = dependency_set.all_versions_for_name(dependency.name)
@@ -8,11 +8,11 @@ module Dependabot
8
8
  @package_json = package_json
9
9
  end
10
10
 
11
- def locked_version(name)
12
- locked = @package_json.fetch("packageManager", nil)
13
- return unless locked
11
+ def requested_version(name)
12
+ version = @package_json.fetch("packageManager", nil)
13
+ return unless version
14
14
 
15
- version_match = locked.match(/#{name}@(?<version>\d+.\d+.\d+)/)
15
+ version_match = version.match(/#{name}@(?<version>\d+.\d+.\d+)/)
16
16
  version_match&.named_captures&.fetch("version", nil)
17
17
  end
18
18
  end
@@ -0,0 +1,75 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ module Dependabot
5
+ module NpmAndYarn
6
+ class RegistryParser
7
+ def initialize(resolved_url:, credentials:)
8
+ @resolved_url = resolved_url
9
+ @credentials = credentials
10
+ end
11
+
12
+ def registry_source_for(name)
13
+ url =
14
+ if resolved_url.include?("/~/")
15
+ # Gemfury format
16
+ resolved_url.split("/~/").first
17
+ elsif resolved_url.include?("/#{name}/-/#{name}")
18
+ # MyGet / Bintray format
19
+ resolved_url.split("/#{name}/-/#{name}").first
20
+ .gsub("dl.bintray.com//", "api.bintray.com/npm/").
21
+ # GitLab format
22
+ gsub(%r{\/projects\/\d+}, "")
23
+ elsif resolved_url.include?("/#{name}/-/#{name.split('/').last}")
24
+ # Sonatype Nexus / Artifactory JFrog format
25
+ resolved_url.split("/#{name}/-/#{name.split('/').last}").first
26
+ elsif (cred_url = url_for_relevant_cred) then cred_url
27
+ else
28
+ resolved_url.split("/")[0..2].join("/")
29
+ end
30
+
31
+ { type: "registry", url: url }
32
+ end
33
+
34
+ def dependency_name
35
+ url_base = if resolved_url.include?("/-/")
36
+ resolved_url.split("/-/").first
37
+ else
38
+ resolved_url
39
+ end
40
+
41
+ url_base.split("/")[3..-1].join("/").gsub("%2F", "/")
42
+ end
43
+
44
+ private
45
+
46
+ attr_reader :resolved_url, :credentials
47
+
48
+ def url_for_relevant_cred
49
+ resolved_url_host = URI(resolved_url).host
50
+
51
+ credential_matching_url =
52
+ credentials
53
+ .select { |cred| cred["type"] == "npm_registry" }
54
+ .sort_by { |cred| cred["registry"].length }
55
+ .find do |details|
56
+ next true if resolved_url_host == details["registry"]
57
+
58
+ uri = if details["registry"]&.include?("://")
59
+ URI(details["registry"])
60
+ else
61
+ URI("https://#{details['registry']}")
62
+ end
63
+ resolved_url_host == uri.host && resolved_url.include?(details["registry"])
64
+ end
65
+
66
+ return unless credential_matching_url
67
+
68
+ # Trim the resolved URL so that it ends at the same point as the
69
+ # credential registry
70
+ reg = credential_matching_url["registry"]
71
+ resolved_url.gsub(/#{Regexp.quote(reg)}.*/, "") + reg
72
+ end
73
+ end
74
+ end
75
+ end