dependabot-bundler 0.138.3 → 0.138.4
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 +4 -4
- data/helpers/v1/run.rb +2 -0
- data/helpers/v2/build +1 -0
- data/helpers/v2/lib/functions.rb +78 -11
- data/helpers/v2/lib/functions/dependency_source.rb +86 -0
- data/helpers/v2/lib/functions/force_updater.rb +167 -0
- data/helpers/v2/lib/functions/lockfile_updater.rb +224 -0
- data/helpers/v2/lib/functions/version_resolver.rb +140 -0
- data/helpers/v2/monkey_patches/definition_ruby_version_patch.rb +2 -2
- data/helpers/v2/run.rb +1 -1
- data/helpers/v2/spec/functions/dependency_source_spec.rb +185 -0
- data/helpers/v2/spec/functions/version_resolver_spec.rb +97 -0
- data/helpers/v2/spec/functions_spec.rb +1 -12
- data/lib/dependabot/bundler/file_updater/requirement_replacer.rb +4 -2
- data/lib/dependabot/bundler/update_checker/requirements_updater.rb +2 -2
- metadata +10 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c508c2f2ed7c44b477686d7385a08b08f9cc3b22821d4fd1a96f6228d7f7b7ae
|
|
4
|
+
data.tar.gz: 89ef7a905d30d6b46191760a893c119b8e45814e8fdd4a6fd6150327ecccf7f2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e91dae1d2fbed29ba408ab4bcc854662b3e36634d425732216e98bc92c881170b475325c0bca09e50803bdc9835cf28456aaf9c59d0620c6ba2c96920a9d3032
|
|
7
|
+
data.tar.gz: 28cba5afca8459dfc9c2e7e9ec64065ed1da409d0391c40847cf9c5353ea21f56da8b3228b0462209789abf08022b40e547e1677df694874d3c708f4bcf33c6c
|
data/helpers/v1/run.rb
CHANGED
data/helpers/v2/build
CHANGED
data/helpers/v2/lib/functions.rb
CHANGED
|
@@ -1,59 +1,107 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
require "functions/conflicting_dependency_resolver"
|
|
4
|
+
require "functions/dependency_source"
|
|
5
|
+
require "functions/file_parser"
|
|
6
|
+
require "functions/force_updater"
|
|
7
|
+
require "functions/lockfile_updater"
|
|
8
|
+
require "functions/version_resolver"
|
|
3
9
|
|
|
4
10
|
module Functions
|
|
5
11
|
class NotImplementedError < StandardError; end
|
|
6
12
|
|
|
7
13
|
def self.parsed_gemfile(lockfile_name:, gemfile_name:, dir:)
|
|
8
14
|
set_bundler_flags_and_credentials(dir: dir, credentials: [],
|
|
9
|
-
|
|
15
|
+
using_bundler2: false)
|
|
10
16
|
FileParser.new(lockfile_name: lockfile_name).
|
|
11
17
|
parsed_gemfile(gemfile_name: gemfile_name)
|
|
12
18
|
end
|
|
13
19
|
|
|
14
20
|
def self.parsed_gemspec(lockfile_name:, gemspec_name:, dir:)
|
|
15
21
|
set_bundler_flags_and_credentials(dir: dir, credentials: [],
|
|
16
|
-
|
|
22
|
+
using_bundler2: false)
|
|
17
23
|
FileParser.new(lockfile_name: lockfile_name).
|
|
18
24
|
parsed_gemspec(gemspec_name: gemspec_name)
|
|
19
25
|
end
|
|
20
26
|
|
|
21
27
|
def self.vendor_cache_dir(dir:)
|
|
22
|
-
|
|
28
|
+
set_bundler_flags_and_credentials(dir: dir, credentials: [],
|
|
29
|
+
using_bundler2: false)
|
|
30
|
+
Bundler.app_cache
|
|
23
31
|
end
|
|
24
32
|
|
|
25
33
|
def self.update_lockfile(dir:, gemfile_name:, lockfile_name:, using_bundler2:,
|
|
26
34
|
credentials:, dependencies:)
|
|
27
|
-
|
|
35
|
+
set_bundler_flags_and_credentials(dir: dir, credentials: credentials,
|
|
36
|
+
using_bundler2: using_bundler2)
|
|
37
|
+
LockfileUpdater.new(
|
|
38
|
+
gemfile_name: gemfile_name,
|
|
39
|
+
lockfile_name: lockfile_name,
|
|
40
|
+
dependencies: dependencies
|
|
41
|
+
).run
|
|
28
42
|
end
|
|
29
43
|
|
|
30
44
|
def self.force_update(dir:, dependency_name:, target_version:, gemfile_name:,
|
|
31
45
|
lockfile_name:, using_bundler2:, credentials:,
|
|
32
46
|
update_multiple_dependencies:)
|
|
33
|
-
|
|
47
|
+
set_bundler_flags_and_credentials(dir: dir, credentials: credentials,
|
|
48
|
+
using_bundler2: using_bundler2)
|
|
49
|
+
ForceUpdater.new(
|
|
50
|
+
dependency_name: dependency_name,
|
|
51
|
+
target_version: target_version,
|
|
52
|
+
gemfile_name: gemfile_name,
|
|
53
|
+
lockfile_name: lockfile_name,
|
|
54
|
+
update_multiple_dependencies: update_multiple_dependencies
|
|
55
|
+
).run
|
|
34
56
|
end
|
|
35
57
|
|
|
36
58
|
def self.dependency_source_type(gemfile_name:, dependency_name:, dir:,
|
|
37
59
|
credentials:)
|
|
38
|
-
|
|
60
|
+
set_bundler_flags_and_credentials(dir: dir, credentials: credentials,
|
|
61
|
+
using_bundler2: false)
|
|
62
|
+
|
|
63
|
+
DependencySource.new(
|
|
64
|
+
gemfile_name: gemfile_name,
|
|
65
|
+
dependency_name: dependency_name
|
|
66
|
+
).type
|
|
39
67
|
end
|
|
40
68
|
|
|
41
69
|
def self.depencency_source_latest_git_version(gemfile_name:, dependency_name:,
|
|
42
70
|
dir:, credentials:,
|
|
43
71
|
dependency_source_url:,
|
|
44
72
|
dependency_source_branch:)
|
|
45
|
-
|
|
73
|
+
set_bundler_flags_and_credentials(dir: dir, credentials: credentials,
|
|
74
|
+
using_bundler2: false)
|
|
75
|
+
DependencySource.new(
|
|
76
|
+
gemfile_name: gemfile_name,
|
|
77
|
+
dependency_name: dependency_name
|
|
78
|
+
).latest_git_version(
|
|
79
|
+
dependency_source_url: dependency_source_url,
|
|
80
|
+
dependency_source_branch: dependency_source_branch
|
|
81
|
+
)
|
|
46
82
|
end
|
|
47
83
|
|
|
48
84
|
def self.private_registry_versions(gemfile_name:, dependency_name:, dir:,
|
|
49
85
|
credentials:)
|
|
50
|
-
|
|
86
|
+
set_bundler_flags_and_credentials(dir: dir, credentials: credentials,
|
|
87
|
+
using_bundler2: false)
|
|
88
|
+
|
|
89
|
+
DependencySource.new(
|
|
90
|
+
gemfile_name: gemfile_name,
|
|
91
|
+
dependency_name: dependency_name
|
|
92
|
+
).private_registry_versions
|
|
51
93
|
end
|
|
52
94
|
|
|
53
95
|
def self.resolve_version(dependency_name:, dependency_requirements:,
|
|
54
96
|
gemfile_name:, lockfile_name:, using_bundler2:,
|
|
55
97
|
dir:, credentials:)
|
|
56
|
-
|
|
98
|
+
set_bundler_flags_and_credentials(dir: dir, credentials: credentials, using_bundler2: using_bundler2)
|
|
99
|
+
VersionResolver.new(
|
|
100
|
+
dependency_name: dependency_name,
|
|
101
|
+
dependency_requirements: dependency_requirements,
|
|
102
|
+
gemfile_name: gemfile_name,
|
|
103
|
+
lockfile_name: lockfile_name
|
|
104
|
+
).version_details
|
|
57
105
|
end
|
|
58
106
|
|
|
59
107
|
def self.jfrog_source(dir:, gemfile_name:, credentials:, using_bundler2:)
|
|
@@ -61,7 +109,26 @@ module Functions
|
|
|
61
109
|
end
|
|
62
110
|
|
|
63
111
|
def self.git_specs(dir:, gemfile_name:, credentials:, using_bundler2:)
|
|
64
|
-
|
|
112
|
+
set_bundler_flags_and_credentials(dir: dir, credentials: credentials,
|
|
113
|
+
using_bundler2: using_bundler2)
|
|
114
|
+
|
|
115
|
+
git_specs = Bundler::Definition.build(gemfile_name, nil, {}).dependencies.
|
|
116
|
+
select do |spec|
|
|
117
|
+
spec.source.is_a?(Bundler::Source::Git)
|
|
118
|
+
end
|
|
119
|
+
git_specs.map do |spec|
|
|
120
|
+
# Piggy-back off some private Bundler methods to configure the
|
|
121
|
+
# URI with auth details in the same way Bundler does.
|
|
122
|
+
git_proxy = spec.source.send(:git_proxy)
|
|
123
|
+
auth_uri = spec.source.uri.gsub("git://", "https://")
|
|
124
|
+
auth_uri = git_proxy.send(:configured_uri_for, auth_uri)
|
|
125
|
+
auth_uri += ".git" unless auth_uri.end_with?(".git")
|
|
126
|
+
auth_uri += "/info/refs?service=git-upload-pack"
|
|
127
|
+
{
|
|
128
|
+
uri: spec.source.uri,
|
|
129
|
+
auth_uri: auth_uri
|
|
130
|
+
}
|
|
131
|
+
end
|
|
65
132
|
end
|
|
66
133
|
|
|
67
134
|
def self.set_bundler_flags_and_credentials(dir:, credentials:,
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
module Functions
|
|
2
|
+
class DependencySource
|
|
3
|
+
attr_reader :gemfile_name, :dependency_name
|
|
4
|
+
|
|
5
|
+
RUBYGEMS = "rubygems"
|
|
6
|
+
PRIVATE_REGISTRY = "private"
|
|
7
|
+
GIT = "git"
|
|
8
|
+
OTHER = "other"
|
|
9
|
+
|
|
10
|
+
def initialize(gemfile_name:, dependency_name:)
|
|
11
|
+
@gemfile_name = gemfile_name
|
|
12
|
+
@dependency_name = dependency_name
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def type
|
|
16
|
+
bundler_source = specified_source || default_source
|
|
17
|
+
type_of(bundler_source)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def latest_git_version(dependency_source_url:, dependency_source_branch:)
|
|
21
|
+
source = Bundler::Source::Git.new(
|
|
22
|
+
"uri" => dependency_source_url,
|
|
23
|
+
"branch" => dependency_source_branch,
|
|
24
|
+
"name" => dependency_name,
|
|
25
|
+
"submodules" => true
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Tell Bundler we're fine with fetching the source remotely
|
|
29
|
+
source.instance_variable_set(:@allow_remote, true)
|
|
30
|
+
|
|
31
|
+
spec = source.specs.first
|
|
32
|
+
{ version: spec.version, commit_sha: spec.source.revision }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def private_registry_versions
|
|
36
|
+
bundler_source = specified_source || default_source
|
|
37
|
+
|
|
38
|
+
bundler_source.
|
|
39
|
+
fetchers.flat_map do |fetcher|
|
|
40
|
+
fetcher.
|
|
41
|
+
specs_with_retry([dependency_name], bundler_source).
|
|
42
|
+
search_all(dependency_name)
|
|
43
|
+
end.
|
|
44
|
+
map(&:version)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def type_of(bundler_source)
|
|
50
|
+
case bundler_source
|
|
51
|
+
when Bundler::Source::Rubygems
|
|
52
|
+
remote = bundler_source.remotes.first
|
|
53
|
+
if remote.nil? || remote.to_s == "https://rubygems.org/"
|
|
54
|
+
RUBYGEMS
|
|
55
|
+
else
|
|
56
|
+
PRIVATE_REGISTRY
|
|
57
|
+
end
|
|
58
|
+
when Bundler::Source::Git
|
|
59
|
+
GIT
|
|
60
|
+
else
|
|
61
|
+
OTHER
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def specified_source
|
|
66
|
+
return @specified_source if defined? @specified_source
|
|
67
|
+
|
|
68
|
+
@specified_source = definition.dependencies.
|
|
69
|
+
find { |dep| dep.name == dependency_name }&.source
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def default_source
|
|
73
|
+
definition.send(:sources).default_source
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def definition
|
|
77
|
+
@definition ||= Bundler::Definition.build(gemfile_name, nil, {})
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def serialize_bundler_source(source)
|
|
81
|
+
{
|
|
82
|
+
type: source.class.to_s
|
|
83
|
+
}
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
module Functions
|
|
2
|
+
class ForceUpdater
|
|
3
|
+
class TransitiveDependencyError < StandardError; end
|
|
4
|
+
|
|
5
|
+
def initialize(dependency_name:, target_version:, gemfile_name:,
|
|
6
|
+
lockfile_name:, update_multiple_dependencies:)
|
|
7
|
+
@dependency_name = dependency_name
|
|
8
|
+
@target_version = target_version
|
|
9
|
+
@gemfile_name = gemfile_name
|
|
10
|
+
@lockfile_name = lockfile_name
|
|
11
|
+
@update_multiple_dependencies = update_multiple_dependencies
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def run
|
|
15
|
+
# Only allow upgrades. Otherwise it's unlikely that this
|
|
16
|
+
# resolution will be found by the FileUpdater
|
|
17
|
+
Bundler.settings.set_command_option(
|
|
18
|
+
"only_update_to_newer_versions",
|
|
19
|
+
true
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
dependencies_to_unlock = []
|
|
23
|
+
|
|
24
|
+
begin
|
|
25
|
+
definition = build_definition(dependencies_to_unlock: dependencies_to_unlock)
|
|
26
|
+
definition.resolve_remotely!
|
|
27
|
+
specs = definition.resolve
|
|
28
|
+
updates = [{ name: dependency_name }] +
|
|
29
|
+
dependencies_to_unlock.map { |dep| { name: dep.name } }
|
|
30
|
+
specs = specs.map do |dep|
|
|
31
|
+
{
|
|
32
|
+
name: dep.name,
|
|
33
|
+
version: dep.version
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
[updates, specs]
|
|
37
|
+
rescue Bundler::VersionConflict => e
|
|
38
|
+
raise unless update_multiple_dependencies?
|
|
39
|
+
|
|
40
|
+
# TODO: Not sure this won't unlock way too many things...
|
|
41
|
+
new_dependencies_to_unlock =
|
|
42
|
+
new_dependencies_to_unlock_from(
|
|
43
|
+
error: e,
|
|
44
|
+
already_unlocked: dependencies_to_unlock
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
raise if new_dependencies_to_unlock.none?
|
|
48
|
+
|
|
49
|
+
dependencies_to_unlock += new_dependencies_to_unlock
|
|
50
|
+
retry
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
attr_reader :dependency_name, :target_version, :gemfile_name,
|
|
57
|
+
:lockfile_name, :credentials,
|
|
58
|
+
:update_multiple_dependencies
|
|
59
|
+
alias update_multiple_dependencies? update_multiple_dependencies
|
|
60
|
+
|
|
61
|
+
def new_dependencies_to_unlock_from(error:, already_unlocked:)
|
|
62
|
+
potentials_deps =
|
|
63
|
+
relevant_conflicts(error, already_unlocked).
|
|
64
|
+
flat_map(&:requirement_trees).
|
|
65
|
+
reject do |tree|
|
|
66
|
+
# If the final requirement wasn't specific, it can't be binding
|
|
67
|
+
next true if tree.last.requirement == Gem::Requirement.new(">= 0")
|
|
68
|
+
|
|
69
|
+
# If the conflict wasn't for the dependency we're updating then
|
|
70
|
+
# we don't have enough info to reject it
|
|
71
|
+
next false unless tree.last.name == dependency_name
|
|
72
|
+
|
|
73
|
+
# If the final requirement *was* for the dependency we're updating
|
|
74
|
+
# then we can ignore the tree if it permits the target version
|
|
75
|
+
tree.last.requirement.satisfied_by?(
|
|
76
|
+
Gem::Version.new(target_version)
|
|
77
|
+
)
|
|
78
|
+
end.map(&:first)
|
|
79
|
+
|
|
80
|
+
potentials_deps.
|
|
81
|
+
reject { |dep| already_unlocked.map(&:name).include?(dep.name) }.
|
|
82
|
+
reject { |dep| [dependency_name, "ruby\0"].include?(dep.name) }.
|
|
83
|
+
uniq
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def relevant_conflicts(error, dependencies_being_unlocked)
|
|
87
|
+
names = [*dependencies_being_unlocked.map(&:name), dependency_name]
|
|
88
|
+
|
|
89
|
+
# For a conflict to be relevant to the updates we're making it must be
|
|
90
|
+
# 1) caused by a new requirement introduced by our unlocking, or
|
|
91
|
+
# 2) caused by an old requirement that prohibits the update.
|
|
92
|
+
# Hence, we look at the beginning and end of the requirement trees
|
|
93
|
+
error.cause.conflicts.values.
|
|
94
|
+
select do |conflict|
|
|
95
|
+
conflict.requirement_trees.any? do |t|
|
|
96
|
+
names.include?(t.last.name) || names.include?(t.first.name)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def build_definition(dependencies_to_unlock:)
|
|
102
|
+
gems_to_unlock = dependencies_to_unlock.map(&:name) + [dependency_name]
|
|
103
|
+
definition = Bundler::Definition.build(
|
|
104
|
+
gemfile_name,
|
|
105
|
+
lockfile_name,
|
|
106
|
+
gems: gems_to_unlock + subdependencies,
|
|
107
|
+
lock_shared_dependencies: true
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Remove the Gemfile / gemspec requirements on the gems we're
|
|
111
|
+
# unlocking (i.e., completely unlock them)
|
|
112
|
+
gems_to_unlock.each do |gem_name|
|
|
113
|
+
unlock_gem(definition: definition, gem_name: gem_name)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
dep = definition.dependencies.
|
|
117
|
+
find { |d| d.name == dependency_name }
|
|
118
|
+
|
|
119
|
+
# If the dependency is not found in the Gemfile it means this is a
|
|
120
|
+
# transitive dependency that we can't force update.
|
|
121
|
+
raise TransitiveDependencyError unless dep
|
|
122
|
+
|
|
123
|
+
# Set the requirement for the gem we're forcing an update of
|
|
124
|
+
new_req = Gem::Requirement.create("= #{target_version}")
|
|
125
|
+
dep.instance_variable_set(:@requirement, new_req)
|
|
126
|
+
dep.source = nil if dep.source.is_a?(Bundler::Source::Git)
|
|
127
|
+
|
|
128
|
+
definition
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def lockfile
|
|
132
|
+
return @lockfile if defined?(@lockfile)
|
|
133
|
+
|
|
134
|
+
@lockfile =
|
|
135
|
+
begin
|
|
136
|
+
return unless lockfile_name && File.exist?(lockfile_name)
|
|
137
|
+
|
|
138
|
+
File.read(lockfile_name)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def subdependencies
|
|
143
|
+
# If there's no lockfile we don't need to worry about
|
|
144
|
+
# subdependencies
|
|
145
|
+
return [] unless lockfile
|
|
146
|
+
|
|
147
|
+
all_deps = Bundler::LockfileParser.new(lockfile).
|
|
148
|
+
specs.map(&:name).map(&:to_s)
|
|
149
|
+
top_level = Bundler::Definition.
|
|
150
|
+
build(gemfile_name, lockfile_name, {}).
|
|
151
|
+
dependencies.map(&:name).map(&:to_s)
|
|
152
|
+
|
|
153
|
+
all_deps - top_level
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def unlock_gem(definition:, gem_name:)
|
|
157
|
+
dep = definition.dependencies.find { |d| d.name == gem_name }
|
|
158
|
+
version = definition.locked_gems.specs.
|
|
159
|
+
find { |d| d.name == gem_name }.version
|
|
160
|
+
|
|
161
|
+
dep&.instance_variable_set(
|
|
162
|
+
:@requirement,
|
|
163
|
+
Gem::Requirement.create(">= #{version}")
|
|
164
|
+
)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Functions
|
|
4
|
+
class LockfileUpdater
|
|
5
|
+
RETRYABLE_ERRORS = [Bundler::HTTPError].freeze
|
|
6
|
+
GEM_NOT_FOUND_ERROR_REGEX =
|
|
7
|
+
/
|
|
8
|
+
locked\sto\s(?<name>[^\s]+)\s\(|
|
|
9
|
+
not\sfind\s(?<name>[^\s]+)-\d|
|
|
10
|
+
has\s(?<name>[^\s]+)\slocked\sat
|
|
11
|
+
/x.freeze
|
|
12
|
+
|
|
13
|
+
def initialize(gemfile_name:, lockfile_name:, dependencies:)
|
|
14
|
+
@gemfile_name = gemfile_name
|
|
15
|
+
@lockfile_name = lockfile_name
|
|
16
|
+
@dependencies = dependencies
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def run
|
|
20
|
+
generate_lockfile
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
attr_reader :gemfile_name, :lockfile_name, :dependencies
|
|
26
|
+
|
|
27
|
+
def generate_lockfile
|
|
28
|
+
dependencies_to_unlock = dependencies.map { |d| d.fetch("name") }
|
|
29
|
+
|
|
30
|
+
begin
|
|
31
|
+
definition = build_definition(dependencies_to_unlock)
|
|
32
|
+
|
|
33
|
+
old_reqs = lock_deps_being_updated_to_exact_versions(definition)
|
|
34
|
+
|
|
35
|
+
definition.resolve_remotely!
|
|
36
|
+
|
|
37
|
+
old_reqs.each do |dep_name, old_req|
|
|
38
|
+
d_dep = definition.dependencies.find { |d| d.name == dep_name }
|
|
39
|
+
if old_req == :none then definition.dependencies.delete(d_dep)
|
|
40
|
+
else
|
|
41
|
+
d_dep.instance_variable_set(:@requirement, old_req)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
cache_vendored_gems(definition) if Bundler.app_cache.exist?
|
|
46
|
+
|
|
47
|
+
definition.to_lock
|
|
48
|
+
rescue Bundler::GemNotFound => e
|
|
49
|
+
unlock_yanked_gem(dependencies_to_unlock, e) && retry
|
|
50
|
+
rescue Bundler::VersionConflict => e
|
|
51
|
+
unlock_blocking_subdeps(dependencies_to_unlock, e) && retry
|
|
52
|
+
rescue *RETRYABLE_ERRORS
|
|
53
|
+
raise if @retrying
|
|
54
|
+
|
|
55
|
+
@retrying = true
|
|
56
|
+
sleep(rand(1.0..5.0))
|
|
57
|
+
retry
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def cache_vendored_gems(definition)
|
|
62
|
+
# Dependencies that have been unlocked for the update (including
|
|
63
|
+
# sub-dependencies)
|
|
64
|
+
unlocked_gems = definition.instance_variable_get(:@unlock).
|
|
65
|
+
fetch(:gems).reject { |gem| __keep_on_prune?(gem) }
|
|
66
|
+
bundler_opts = {
|
|
67
|
+
cache_all: true,
|
|
68
|
+
cache_all_platforms: true,
|
|
69
|
+
no_prune: true
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
Bundler.settings.temporary(**bundler_opts) do
|
|
73
|
+
# Fetch and cache gems on all platforms without pruning
|
|
74
|
+
Bundler::Runtime.new(nil, definition).cache
|
|
75
|
+
|
|
76
|
+
# Only prune unlocked gems (the original implementation is in
|
|
77
|
+
# Bundler::Runtime)
|
|
78
|
+
cache_path = Bundler.app_cache
|
|
79
|
+
resolve = definition.resolve
|
|
80
|
+
prune_gem_cache(resolve, cache_path, unlocked_gems)
|
|
81
|
+
prune_git_and_path_cache(resolve, cache_path)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# This is not officially supported and may be removed without notice.
|
|
86
|
+
def __keep_on_prune?(spec_name)
|
|
87
|
+
unless (specs = Bundler.settings[:persistent_gems_after_clean])
|
|
88
|
+
return false
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
specs.include?(spec_name)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Copied from Bundler::Runtime: Modified to only prune gems that have
|
|
95
|
+
# been unlocked
|
|
96
|
+
def prune_gem_cache(resolve, cache_path, unlocked_gems)
|
|
97
|
+
cached_gems = Dir["#{cache_path}/*.gem"]
|
|
98
|
+
|
|
99
|
+
outdated_gems = cached_gems.reject do |path|
|
|
100
|
+
spec = Bundler.rubygems.spec_from_gem path
|
|
101
|
+
|
|
102
|
+
!unlocked_gems.include?(spec.name) || resolve.any? do |s|
|
|
103
|
+
s.name == spec.name && s.version == spec.version &&
|
|
104
|
+
!s.source.is_a?(Bundler::Source::Git)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
return unless outdated_gems.any?
|
|
109
|
+
|
|
110
|
+
outdated_gems.each do |path|
|
|
111
|
+
File.delete(path)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Copied from Bundler::Runtime
|
|
116
|
+
def prune_git_and_path_cache(resolve, cache_path)
|
|
117
|
+
cached_git_and_path = Dir["#{cache_path}/*/.bundlecache"]
|
|
118
|
+
|
|
119
|
+
outdated_git_and_path = cached_git_and_path.reject do |path|
|
|
120
|
+
name = File.basename(File.dirname(path))
|
|
121
|
+
|
|
122
|
+
resolve.any? do |s|
|
|
123
|
+
s.source.respond_to?(:app_cache_dirname) &&
|
|
124
|
+
s.source.app_cache_dirname == name
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
return unless outdated_git_and_path.any?
|
|
129
|
+
|
|
130
|
+
outdated_git_and_path.each do |path|
|
|
131
|
+
path = File.dirname(path)
|
|
132
|
+
FileUtils.rm_rf(path)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def unlock_yanked_gem(dependencies_to_unlock, error)
|
|
137
|
+
raise unless error.message.match?(GEM_NOT_FOUND_ERROR_REGEX)
|
|
138
|
+
|
|
139
|
+
gem_name = error.message.match(GEM_NOT_FOUND_ERROR_REGEX).
|
|
140
|
+
named_captures["name"]
|
|
141
|
+
raise if dependencies_to_unlock.include?(gem_name)
|
|
142
|
+
|
|
143
|
+
dependencies_to_unlock << gem_name
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
|
147
|
+
def unlock_blocking_subdeps(dependencies_to_unlock, error)
|
|
148
|
+
all_deps = Bundler::LockfileParser.new(lockfile).
|
|
149
|
+
specs.map(&:name).map(&:to_s)
|
|
150
|
+
top_level = build_definition([]).dependencies.
|
|
151
|
+
map(&:name).map(&:to_s)
|
|
152
|
+
allowed_new_unlocks = all_deps - top_level - dependencies_to_unlock
|
|
153
|
+
|
|
154
|
+
raise if allowed_new_unlocks.none?
|
|
155
|
+
|
|
156
|
+
# Unlock any sub-dependencies that Bundler reports caused the
|
|
157
|
+
# conflict
|
|
158
|
+
potentials_deps =
|
|
159
|
+
error.cause.conflicts.values.
|
|
160
|
+
flat_map(&:requirement_trees).
|
|
161
|
+
map do |tree|
|
|
162
|
+
tree.find { |req| allowed_new_unlocks.include?(req.name) }
|
|
163
|
+
end.compact.map(&:name)
|
|
164
|
+
|
|
165
|
+
# If there are specific dependencies we can unlock, unlock them
|
|
166
|
+
return dependencies_to_unlock.append(*potentials_deps) if potentials_deps.any?
|
|
167
|
+
|
|
168
|
+
# Fall back to unlocking *all* sub-dependencies. This is required
|
|
169
|
+
# because Bundler's VersionConflict objects don't include enough
|
|
170
|
+
# information to chart the full path through all conflicts unwound
|
|
171
|
+
dependencies_to_unlock.append(*allowed_new_unlocks)
|
|
172
|
+
end
|
|
173
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
|
174
|
+
|
|
175
|
+
def build_definition(dependencies_to_unlock)
|
|
176
|
+
defn = Bundler::Definition.build(
|
|
177
|
+
gemfile_name,
|
|
178
|
+
lockfile_name,
|
|
179
|
+
gems: dependencies_to_unlock
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Bundler unlocks the sub-dependencies of gems it is passed even
|
|
183
|
+
# if those sub-deps are top-level dependencies. We only want true
|
|
184
|
+
# subdeps unlocked, like they were in the UpdateChecker, so we
|
|
185
|
+
# mutate the unlocked gems array.
|
|
186
|
+
unlocked = defn.instance_variable_get(:@unlock).fetch(:gems)
|
|
187
|
+
must_not_unlock = defn.dependencies.map(&:name).map(&:to_s) -
|
|
188
|
+
dependencies_to_unlock
|
|
189
|
+
unlocked.reject! { |n| must_not_unlock.include?(n) }
|
|
190
|
+
|
|
191
|
+
defn
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def lock_deps_being_updated_to_exact_versions(definition)
|
|
195
|
+
dependencies.each_with_object({}) do |dep, old_reqs|
|
|
196
|
+
defn_dep = definition.dependencies.find do |d|
|
|
197
|
+
d.name == dep.fetch("name")
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
if defn_dep.nil?
|
|
201
|
+
definition.dependencies <<
|
|
202
|
+
Bundler::Dependency.new(dep.fetch("name"), dep.fetch("version"))
|
|
203
|
+
old_reqs[dep.fetch("name")] = :none
|
|
204
|
+
elsif git_dependency?(dep) &&
|
|
205
|
+
defn_dep.source.is_a?(Bundler::Source::Git)
|
|
206
|
+
defn_dep.source.unlock!
|
|
207
|
+
elsif Gem::Version.correct?(dep.fetch("version"))
|
|
208
|
+
new_req = Gem::Requirement.create("= #{dep.fetch('version')}")
|
|
209
|
+
old_reqs[dep.fetch("name")] = defn_dep.requirement
|
|
210
|
+
defn_dep.instance_variable_set(:@requirement, new_req)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def git_dependency?(dep)
|
|
216
|
+
sources = dep.fetch("requirements").map { |r| r.fetch("source") }
|
|
217
|
+
sources.all? { |s| s&.fetch("type", nil) == "git" }
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def lockfile
|
|
221
|
+
@lockfile ||= File.read(lockfile_name)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
@@ -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("
|
|
11
|
+
Gem::Specification.new("Ruby\0", requested_version)
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
sources.metadata_source.specs <<
|
|
15
|
-
Gem::Specification.new("
|
|
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("
|
|
5
|
+
$LOAD_PATH.unshift(File.expand_path("./monkey_patches", __dir__))
|
|
6
6
|
|
|
7
7
|
# Bundler monkey patches
|
|
8
8
|
require "definition_ruby_version_patch"
|
|
@@ -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
|
|
@@ -5,18 +5,7 @@ require "native_spec_helper"
|
|
|
5
5
|
RSpec.describe Functions do
|
|
6
6
|
# Verify v1 method signatures are exist, but raise as NYI
|
|
7
7
|
{
|
|
8
|
-
|
|
9
|
-
update_lockfile: [ :dir, :gemfile_name, :lockfile_name, :using_bundler2, :credentials, :dependencies ],
|
|
10
|
-
force_update: [ :dir, :dependency_name, :target_version, :gemfile_name, :lockfile_name, :using_bundler2,
|
|
11
|
-
:credentials, :update_multiple_dependencies ],
|
|
12
|
-
dependency_source_type: [ :gemfile_name, :dependency_name, :dir, :credentials ],
|
|
13
|
-
depencency_source_latest_git_version: [ :gemfile_name, :dependency_name, :dir, :credentials, :dependency_source_url,
|
|
14
|
-
:dependency_source_branch ],
|
|
15
|
-
private_registry_versions: [:gemfile_name, :dependency_name, :dir, :credentials ],
|
|
16
|
-
resolve_version: [:dependency_name, :dependency_requirements, :gemfile_name, :lockfile_name, :using_bundler2,
|
|
17
|
-
:dir, :credentials],
|
|
18
|
-
jfrog_source: [:dir, :gemfile_name, :credentials, :using_bundler2],
|
|
19
|
-
git_specs: [:dir, :gemfile_name, :credentials, :using_bundler2],
|
|
8
|
+
jfrog_source: %i(dir gemfile_name credentials using_bundler2)
|
|
20
9
|
}.each do |function, kwargs|
|
|
21
10
|
describe "::#{function}" do
|
|
22
11
|
let(:args) do
|
|
@@ -167,6 +167,8 @@ module Dependabot
|
|
|
167
167
|
req_string.include?(" ")
|
|
168
168
|
end
|
|
169
169
|
|
|
170
|
+
EQUALITY_OPERATOR = /(?<![<>!])=/.freeze
|
|
171
|
+
|
|
170
172
|
def use_equality_operator?(requirement_nodes)
|
|
171
173
|
return true if requirement_nodes.none?
|
|
172
174
|
|
|
@@ -178,7 +180,7 @@ module Dependabot
|
|
|
178
180
|
requirement_nodes.first.children.first.loc.expression.source
|
|
179
181
|
end
|
|
180
182
|
|
|
181
|
-
req_string.match?(
|
|
183
|
+
req_string.match?(EQUALITY_OPERATOR)
|
|
182
184
|
end
|
|
183
185
|
|
|
184
186
|
def new_requirement_string(quote_characters:,
|
|
@@ -203,7 +205,7 @@ module Dependabot
|
|
|
203
205
|
# Gem::Requirement serializes exact matches as a string starting
|
|
204
206
|
# with `=`. We may need to remove that equality operator if it
|
|
205
207
|
# wasn't used originally.
|
|
206
|
-
tmp_req = tmp_req.gsub(
|
|
208
|
+
tmp_req = tmp_req.gsub(EQUALITY_OPERATOR, "") unless use_equality_operator
|
|
207
209
|
|
|
208
210
|
tmp_req.strip
|
|
209
211
|
end
|
|
@@ -188,7 +188,7 @@ module Dependabot
|
|
|
188
188
|
req
|
|
189
189
|
end
|
|
190
190
|
when "<", "<=" then [update_greatest_version(req, latest_version)]
|
|
191
|
-
when "~>" then
|
|
191
|
+
when "~>" then convert_twiddle_to_range(req, latest_version)
|
|
192
192
|
when "!=" then []
|
|
193
193
|
when ">", ">=" then raise UnfixableRequirement
|
|
194
194
|
else raise "Unexpected operation for requirement: #{op}"
|
|
@@ -214,7 +214,7 @@ module Dependabot
|
|
|
214
214
|
end
|
|
215
215
|
end
|
|
216
216
|
|
|
217
|
-
def
|
|
217
|
+
def convert_twiddle_to_range(requirement, version_to_be_permitted)
|
|
218
218
|
version = requirement.requirements.first.last
|
|
219
219
|
version = version.release if version.prerelease?
|
|
220
220
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dependabot-bundler
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.138.
|
|
4
|
+
version: 0.138.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dependabot
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2021-03-
|
|
11
|
+
date: 2021-03-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: dependabot-common
|
|
@@ -16,14 +16,14 @@ dependencies:
|
|
|
16
16
|
requirements:
|
|
17
17
|
- - '='
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: 0.138.
|
|
19
|
+
version: 0.138.4
|
|
20
20
|
type: :runtime
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
24
|
- - '='
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: 0.138.
|
|
26
|
+
version: 0.138.4
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
28
|
name: byebug
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -211,13 +211,19 @@ files:
|
|
|
211
211
|
- helpers/v2/build
|
|
212
212
|
- helpers/v2/lib/functions.rb
|
|
213
213
|
- helpers/v2/lib/functions/conflicting_dependency_resolver.rb
|
|
214
|
+
- helpers/v2/lib/functions/dependency_source.rb
|
|
214
215
|
- helpers/v2/lib/functions/file_parser.rb
|
|
216
|
+
- helpers/v2/lib/functions/force_updater.rb
|
|
217
|
+
- helpers/v2/lib/functions/lockfile_updater.rb
|
|
218
|
+
- helpers/v2/lib/functions/version_resolver.rb
|
|
215
219
|
- helpers/v2/monkey_patches/definition_bundler_version_patch.rb
|
|
216
220
|
- helpers/v2/monkey_patches/definition_ruby_version_patch.rb
|
|
217
221
|
- helpers/v2/monkey_patches/git_source_patch.rb
|
|
218
222
|
- helpers/v2/run.rb
|
|
219
223
|
- helpers/v2/spec/functions/conflicting_dependency_resolver_spec.rb
|
|
224
|
+
- helpers/v2/spec/functions/dependency_source_spec.rb
|
|
220
225
|
- helpers/v2/spec/functions/file_parser_spec.rb
|
|
226
|
+
- helpers/v2/spec/functions/version_resolver_spec.rb
|
|
221
227
|
- helpers/v2/spec/functions_spec.rb
|
|
222
228
|
- helpers/v2/spec/native_spec_helper.rb
|
|
223
229
|
- helpers/v2/spec/shared_contexts.rb
|