dependabot-bundler 0.138.3 → 0.139.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,140 @@
1
+ module Functions
2
+ class VersionResolver
3
+ GEM_NOT_FOUND_ERROR_REGEX = /locked to (?<name>[^\s]+) \(/.freeze
4
+
5
+ attr_reader :dependency_name, :dependency_requirements,
6
+ :gemfile_name, :lockfile_name
7
+
8
+ def initialize(dependency_name:, dependency_requirements:,
9
+ gemfile_name:, lockfile_name:)
10
+ @dependency_name = dependency_name
11
+ @dependency_requirements = dependency_requirements
12
+ @gemfile_name = gemfile_name
13
+ @lockfile_name = lockfile_name
14
+ end
15
+
16
+ def version_details
17
+ dep = dependency_from_definition
18
+
19
+ # If the dependency wasn't found in the definition, but *is*
20
+ # included in a gemspec, it's because the Gemfile didn't import
21
+ # the gemspec. This is unusual, but the correct behaviour if/when
22
+ # it happens is to behave as if the repo was gemspec-only.
23
+ if dep.nil? && dependency_requirements.any?
24
+ return "latest"
25
+ end
26
+
27
+ # Otherwise, if the dependency wasn't found it's because it is a
28
+ # subdependency that was removed when attempting to update it.
29
+ return nil if dep.nil?
30
+
31
+ # If the dependency is Bundler itself then we can't trust the
32
+ # version that has been returned (it's the version Dependabot is
33
+ # running on, rather than the true latest resolvable version).
34
+ return nil if dep.name == "bundler"
35
+
36
+ details = {
37
+ version: dep.version,
38
+ ruby_version: ruby_version,
39
+ fetcher: fetcher_class(dep)
40
+ }
41
+ if dep.source.instance_of?(::Bundler::Source::Git)
42
+ details[:commit_sha] = dep.source.revision
43
+ end
44
+ details
45
+ end
46
+
47
+ private
48
+
49
+ # rubocop:disable Metrics/PerceivedComplexity
50
+ def dependency_from_definition(unlock_subdependencies: true)
51
+ dependencies_to_unlock = [dependency_name]
52
+ dependencies_to_unlock += subdependencies if unlock_subdependencies
53
+ begin
54
+ definition = build_definition(dependencies_to_unlock)
55
+ definition.resolve_remotely!
56
+ rescue ::Bundler::GemNotFound => e
57
+ unlock_yanked_gem(dependencies_to_unlock, e) && retry
58
+ rescue ::Bundler::HTTPError => e
59
+ # Retry network errors
60
+ # Note: in_a_native_bundler_context will also retry `Bundler::HTTPError` errors
61
+ # up to three times meaning we'll end up retrying this error up to six times
62
+ # TODO: Could we get rid of this retry logic and only rely on
63
+ # SharedBundlerHelpers.in_a_native_bundler_context
64
+ attempt ||= 1
65
+ attempt += 1
66
+ raise if attempt > 3 || !e.message.include?("Network error")
67
+
68
+ retry
69
+ end
70
+
71
+ dep = definition.resolve.find { |d| d.name == dependency_name }
72
+ return dep if dep
73
+ return if dependency_requirements.any? || !unlock_subdependencies
74
+
75
+ # If no definition was found and we're updating a sub-dependency,
76
+ # try again but without unlocking any other sub-dependencies
77
+ dependency_from_definition(unlock_subdependencies: false)
78
+ end
79
+ # rubocop:enable Metrics/PerceivedComplexity
80
+
81
+ def subdependencies
82
+ # If there's no lockfile we don't need to worry about
83
+ # subdependencies
84
+ return [] unless lockfile
85
+
86
+ all_deps = ::Bundler::LockfileParser.new(lockfile).
87
+ specs.map(&:name).map(&:to_s).uniq
88
+ top_level = build_definition([]).dependencies.
89
+ map(&:name).map(&:to_s)
90
+
91
+ all_deps - top_level
92
+ end
93
+
94
+ def build_definition(dependencies_to_unlock)
95
+ # Note: we lock shared dependencies to avoid any top-level
96
+ # dependencies getting unlocked (which would happen if they were
97
+ # also subdependencies of the dependency being unlocked)
98
+ ::Bundler::Definition.build(
99
+ gemfile_name,
100
+ lockfile_name,
101
+ gems: dependencies_to_unlock,
102
+ lock_shared_dependencies: true
103
+ )
104
+ end
105
+
106
+ def unlock_yanked_gem(dependencies_to_unlock, error)
107
+ raise unless error.message.match?(GEM_NOT_FOUND_ERROR_REGEX)
108
+
109
+ gem_name = error.message.match(GEM_NOT_FOUND_ERROR_REGEX).
110
+ named_captures["name"]
111
+ raise if dependencies_to_unlock.include?(gem_name)
112
+
113
+ dependencies_to_unlock << gem_name
114
+ end
115
+
116
+ def lockfile
117
+ return @lockfile if defined?(@lockfile)
118
+
119
+ @lockfile =
120
+ begin
121
+ return unless lockfile_name
122
+ return unless File.exist?(lockfile_name)
123
+
124
+ File.read(lockfile_name)
125
+ end
126
+ end
127
+
128
+ def fetcher_class(dep)
129
+ return unless dep.source.is_a?(::Bundler::Source::Rubygems)
130
+
131
+ dep.source.fetchers.first.fetchers.first.class.to_s
132
+ end
133
+
134
+ def ruby_version
135
+ return nil unless gemfile_name
136
+
137
+ @ruby_version ||= build_definition([]).ruby_version&.gem_version
138
+ end
139
+ end
140
+ end
@@ -8,11 +8,11 @@ module BundlerDefinitionRubyVersionPatch
8
8
  if ruby_version
9
9
  requested_version = ruby_version.to_gem_version_with_patchlevel
10
10
  sources.metadata_source.specs <<
11
- Gem::Specification.new("ruby\0", requested_version)
11
+ Gem::Specification.new("Ruby\0", requested_version)
12
12
  end
13
13
 
14
14
  sources.metadata_source.specs <<
15
- Gem::Specification.new("ruby\0", "2.5.3p105")
15
+ Gem::Specification.new("Ruby\0", "2.5.3p105")
16
16
  end
17
17
  end
18
18
  end
data/helpers/v2/run.rb CHANGED
@@ -2,7 +2,7 @@ require "bundler"
2
2
  require "json"
3
3
 
4
4
  $LOAD_PATH.unshift(File.expand_path("./lib", __dir__))
5
- $LOAD_PATH.unshift(File.expand_path("../v1/monkey_patches", __dir__))
5
+ $LOAD_PATH.unshift(File.expand_path("./monkey_patches", __dir__))
6
6
 
7
7
  # Bundler monkey patches
8
8
  require "definition_ruby_version_patch"
@@ -11,7 +11,7 @@ require "git_source_patch"
11
11
 
12
12
  require "functions"
13
13
 
14
- MIN_BUNDLER_VERSION = "2.0.0"
14
+ MIN_BUNDLER_VERSION = "2.1.0"
15
15
 
16
16
  def validate_bundler_version!
17
17
  return true if correct_bundler_version?
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "native_spec_helper"
4
+ require "shared_contexts"
5
+
6
+ RSpec.describe Functions::DependencySource do
7
+ include_context "in a temporary bundler directory"
8
+
9
+ let(:dependency_source) do
10
+ described_class.new(
11
+ gemfile_name: "Gemfile",
12
+ dependency_name: dependency_name
13
+ )
14
+ end
15
+
16
+ let(:dependency_name) { "business" }
17
+
18
+ let(:project_name) { "specified_source_no_lockfile" }
19
+ let(:registry_url) { "https://repo.fury.io/greysteil/" }
20
+ let(:gemfury_business_url) do
21
+ "https://repo.fury.io/greysteil/api/v1/dependencies?gems=business"
22
+ end
23
+
24
+ before do
25
+ stub_request(:get, registry_url + "versions").
26
+ with(basic_auth: ["SECRET_CODES", ""]).
27
+ to_return(status: 404)
28
+ stub_request(:get, registry_url + "api/v1/dependencies").
29
+ with(basic_auth: ["SECRET_CODES", ""]).
30
+ to_return(status: 200)
31
+ stub_request(:get, gemfury_business_url).
32
+ with(basic_auth: ["SECRET_CODES", ""]).
33
+ to_return(status: 200, body: fixture("ruby", "gemfury_response"))
34
+ end
35
+
36
+ describe "#private_registry_versions" do
37
+ subject(:private_registry_versions) do
38
+ in_tmp_folder { dependency_source.private_registry_versions }
39
+ end
40
+
41
+ it "returns all versions from the private source" do
42
+ is_expected.to eq([
43
+ Gem::Version.new("1.5.0"),
44
+ Gem::Version.new("1.9.0"),
45
+ Gem::Version.new("1.10.0.beta")
46
+ ])
47
+ end
48
+
49
+ context "specified as the default source" do
50
+ let(:project_name) { "specified_default_source_no_lockfile" }
51
+
52
+ it "returns all versions from the private source" do
53
+ is_expected.to eq([
54
+ Gem::Version.new("1.5.0"),
55
+ Gem::Version.new("1.9.0"),
56
+ Gem::Version.new("1.10.0.beta")
57
+ ])
58
+ end
59
+ end
60
+
61
+ context "that we don't have authentication details for" do
62
+ before do
63
+ stub_request(:get, registry_url + "versions").
64
+ with(basic_auth: ["SECRET_CODES", ""]).
65
+ to_return(status: 401)
66
+ stub_request(:get, registry_url + "api/v1/dependencies").
67
+ with(basic_auth: ["SECRET_CODES", ""]).
68
+ to_return(status: 401)
69
+ stub_request(:get, registry_url + "specs.4.8.gz").
70
+ with(basic_auth: ["SECRET_CODES", ""]).
71
+ to_return(status: 401)
72
+ end
73
+
74
+ it "blows up with a useful error" do
75
+ error_class = Bundler::Fetcher::BadAuthenticationError
76
+ expect { private_registry_versions }.
77
+ to raise_error do |error|
78
+ expect(error).to be_a(error_class)
79
+ expect(error.message).to include("Bad username or password for")
80
+ end
81
+ end
82
+ end
83
+
84
+ context "that we have bad authentication details for" do
85
+ before do
86
+ stub_request(:get, registry_url + "versions").
87
+ with(basic_auth: ["SECRET_CODES", ""]).
88
+ to_return(status: 403)
89
+ stub_request(:get, registry_url + "api/v1/dependencies").
90
+ with(basic_auth: ["SECRET_CODES", ""]).
91
+ to_return(status: 403)
92
+ stub_request(:get, registry_url + "specs.4.8.gz").
93
+ with(basic_auth: ["SECRET_CODES", ""]).
94
+ to_return(status: 403)
95
+ end
96
+
97
+ it "blows up with a useful error" do
98
+ error_class = Bundler::Fetcher::BadAuthenticationError
99
+ expect { private_registry_versions }.
100
+ to raise_error do |error|
101
+ expect(error).to be_a(error_class)
102
+ expect(error.message).to include("Bad username or password for")
103
+ end
104
+ end
105
+ end
106
+
107
+ context "that bad-requested, but was a private repo" do
108
+ before do
109
+ stub_request(:get, registry_url + "versions").
110
+ with(basic_auth: ["SECRET_CODES", ""]).
111
+ to_return(status: 400)
112
+ stub_request(:get, registry_url + "api/v1/dependencies").
113
+ with(basic_auth: ["SECRET_CODES", ""]).
114
+ to_return(status: 400)
115
+ stub_request(:get, registry_url + "specs.4.8.gz").
116
+ with(basic_auth: ["SECRET_CODES", ""]).
117
+ to_return(status: 400)
118
+ end
119
+
120
+ it "blows up with a useful error" do
121
+ expect { private_registry_versions }.
122
+ to raise_error do |error|
123
+ expect(error).to be_a(Bundler::HTTPError)
124
+ expect(error.message).
125
+ to include("Could not fetch specs from")
126
+ end
127
+ end
128
+ end
129
+
130
+ context "that doesn't have details of the gem" do
131
+ before do
132
+ stub_request(:get, gemfury_business_url).
133
+ with(basic_auth: ["SECRET_CODES", ""]).
134
+ to_return(status: 404)
135
+
136
+ # Stub indexes to return details of other gems (but not this one)
137
+ stub_request(:get, registry_url + "specs.4.8.gz").
138
+ to_return(
139
+ status: 200,
140
+ body: fixture("ruby", "contribsys_old_index_response")
141
+ )
142
+ stub_request(:get, registry_url + "prerelease_specs.4.8.gz").
143
+ to_return(
144
+ status: 200,
145
+ body: fixture("ruby", "contribsys_old_index_prerelease_response")
146
+ )
147
+ end
148
+
149
+ it { is_expected.to be_empty }
150
+ end
151
+
152
+ context "that only implements the old Bundler index format..." do
153
+ let(:project_name) { "sidekiq_pro" }
154
+ let(:dependency_name) { "sidekiq-pro" }
155
+ let(:registry_url) { "https://gems.contribsys.com/" }
156
+
157
+ before do
158
+ stub_request(:get, registry_url + "versions").
159
+ with(basic_auth: %w(username password)).
160
+ to_return(status: 404)
161
+ stub_request(:get, registry_url + "api/v1/dependencies").
162
+ with(basic_auth: %w(username password)).
163
+ to_return(status: 404)
164
+ stub_request(:get, registry_url + "specs.4.8.gz").
165
+ with(basic_auth: %w(username password)).
166
+ to_return(
167
+ status: 200,
168
+ body: fixture("ruby", "contribsys_old_index_response")
169
+ )
170
+ stub_request(:get, registry_url + "prerelease_specs.4.8.gz").
171
+ with(basic_auth: %w(username password)).
172
+ to_return(
173
+ status: 200,
174
+ body: fixture("ruby", "contribsys_old_index_prerelease_response")
175
+ )
176
+ end
177
+
178
+ it "returns all versions from the private source" do
179
+ expect(private_registry_versions.length).to eql(70)
180
+ expect(private_registry_versions.min).to eql(Gem::Version.new("1.0.0"))
181
+ expect(private_registry_versions.max).to eql(Gem::Version.new("3.5.2"))
182
+ end
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "native_spec_helper"
4
+ require "shared_contexts"
5
+
6
+ RSpec.describe Functions::VersionResolver do
7
+ include_context "in a temporary bundler directory"
8
+ include_context "stub rubygems compact index"
9
+
10
+ let(:version_resolver) do
11
+ described_class.new(
12
+ dependency_name: dependency_name,
13
+ dependency_requirements: dependency_requirements,
14
+ gemfile_name: "Gemfile",
15
+ lockfile_name: "Gemfile.lock"
16
+ )
17
+ end
18
+
19
+ let(:dependency_name) { "business" }
20
+ let(:dependency_requirements) do
21
+ [{
22
+ file: "Gemfile",
23
+ requirement: requirement_string,
24
+ groups: [],
25
+ source: source
26
+ }]
27
+ end
28
+ let(:source) { nil }
29
+
30
+ let(:rubygems_url) { "https://index.rubygems.org/api/v1/" }
31
+ let(:old_index_url) { rubygems_url + "dependencies" }
32
+
33
+ describe "#version_details" do
34
+ subject do
35
+ in_tmp_folder { version_resolver.version_details }
36
+ end
37
+
38
+ let(:project_name) { "gemfile" }
39
+ let(:requirement_string) { " >= 0" }
40
+
41
+ its([:version]) { is_expected.to eq(Gem::Version.new("1.4.0")) }
42
+ its([:fetcher]) { is_expected.to eq("Bundler::Fetcher::CompactIndex") }
43
+
44
+ context "with a private gemserver source" do
45
+ include_context "stub rubygems compact index"
46
+
47
+ let(:project_name) { "specified_source" }
48
+ let(:requirement_string) { ">= 0" }
49
+
50
+ before do
51
+ gemfury_url = "https://repo.fury.io/greysteil/"
52
+ gemfury_deps_url = gemfury_url + "api/v1/dependencies"
53
+
54
+ stub_request(:get, gemfury_url + "versions").
55
+ to_return(status: 200, body: fixture("ruby", "gemfury-index"))
56
+ stub_request(:get, gemfury_url + "info/business").to_return(status: 404)
57
+ stub_request(:get, gemfury_deps_url).to_return(status: 200)
58
+ stub_request(:get, gemfury_deps_url + "?gems=business,statesman").
59
+ to_return(status: 200, body: fixture("ruby", "gemfury_response"))
60
+ stub_request(:get, gemfury_deps_url + "?gems=business").
61
+ to_return(status: 200, body: fixture("ruby", "gemfury_response"))
62
+ stub_request(:get, gemfury_deps_url + "?gems=statesman").
63
+ to_return(status: 200, body: fixture("ruby", "gemfury_response"))
64
+ end
65
+
66
+ its([:version]) { is_expected.to eq(Gem::Version.new("1.9.0")) }
67
+ its([:fetcher]) { is_expected.to eq("Bundler::Fetcher::Dependency") }
68
+ end
69
+
70
+ context "with a git source" do
71
+ let(:project_name) { "git_source" }
72
+
73
+ its([:version]) { is_expected.to eq(Gem::Version.new("1.6.0")) }
74
+ its([:fetcher]) { is_expected.to be_nil }
75
+ end
76
+
77
+ context "when Bundler's compact index is down" do
78
+ before do
79
+ stub_request(:get, "https://index.rubygems.org/versions").
80
+ to_return(status: 500, body: "We'll be back soon")
81
+ stub_request(:get, "https://index.rubygems.org/info/public_suffix").
82
+ to_return(status: 500, body: "We'll be back soon")
83
+ stub_request(:get, old_index_url).to_return(status: 200)
84
+ stub_request(:get, old_index_url + "?gems=business,statesman").
85
+ to_return(
86
+ status: 200,
87
+ body: fixture("ruby",
88
+ "rubygems_responses",
89
+ "dependencies-default-gemfile")
90
+ )
91
+ end
92
+
93
+ its([:version]) { is_expected.to eq(Gem::Version.new("1.4.0")) }
94
+ its([:fetcher]) { is_expected.to eq("Bundler::Fetcher::Dependency") }
95
+ end
96
+ end
97
+ end