diffend-monitor 0.2.27
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/.coditsu/ci.yml +3 -0
- data/.diffend.yml +3 -0
- data/.github/workflows/ci.yml +64 -0
- data/.gitignore +58 -0
- data/.rspec +1 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +75 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +48 -0
- data/LICENSE.md +57 -0
- data/README.md +25 -0
- data/bin/bundle +114 -0
- data/bin/byebug +29 -0
- data/bin/htmldiff +29 -0
- data/bin/ldiff +29 -0
- data/bin/rake +29 -0
- data/bin/rspec +29 -0
- data/certs/mensfeld.pem +25 -0
- data/certs/tomaszpajor.pem +25 -0
- data/diffend.gemspec +28 -0
- data/lib/diffend.rb +142 -0
- data/lib/diffend/build_bundler_definition.rb +26 -0
- data/lib/diffend/commands.rb +13 -0
- data/lib/diffend/config/fetcher.rb +117 -0
- data/lib/diffend/config/file_finder.rb +38 -0
- data/lib/diffend/config/validator.rb +25 -0
- data/lib/diffend/errors.rb +27 -0
- data/lib/diffend/handle_errors/build_exception_payload.rb +30 -0
- data/lib/diffend/handle_errors/display_to_stdout.rb +17 -0
- data/lib/diffend/handle_errors/messages.rb +29 -0
- data/lib/diffend/handle_errors/report.rb +71 -0
- data/lib/diffend/monitor.rb +28 -0
- data/lib/diffend/request.rb +185 -0
- data/lib/diffend/request_object.rb +6 -0
- data/lib/diffend/track.rb +104 -0
- data/lib/diffend/voting.rb +132 -0
- data/lib/diffend/voting/versions/local.rb +304 -0
- data/lib/diffend/voting/versions/remote.rb +216 -0
- data/plugins.rb +5 -0
- data/scripts/generate_payload_for_file.rb +15 -0
- metadata +138 -0
- metadata.gz.sig +0 -0
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Diffend
|
4
|
+
# Track what is run in production
|
5
|
+
class Track
|
6
|
+
# Time that we want to wait between track requests
|
7
|
+
TRACK_SLEEP = 15
|
8
|
+
# Time that we want to wait before we retry
|
9
|
+
RETRY_SLEEP = 15
|
10
|
+
|
11
|
+
# Initialize tracking
|
12
|
+
def initialize
|
13
|
+
@mutex = Mutex.new
|
14
|
+
@config = fetch_config
|
15
|
+
end
|
16
|
+
|
17
|
+
# Start tracking
|
18
|
+
def start
|
19
|
+
response = exec_request
|
20
|
+
|
21
|
+
perform(response['id'])
|
22
|
+
rescue Diffend::Errors::HandledException
|
23
|
+
sleep(RETRY_SLEEP)
|
24
|
+
|
25
|
+
retry
|
26
|
+
rescue StandardError => e
|
27
|
+
Diffend::HandleErrors::Report.call(
|
28
|
+
exception: e,
|
29
|
+
config: @config,
|
30
|
+
message: :unhandled_exception,
|
31
|
+
report: true,
|
32
|
+
raise_exception: false
|
33
|
+
)
|
34
|
+
|
35
|
+
sleep(RETRY_SLEEP)
|
36
|
+
|
37
|
+
retry
|
38
|
+
end
|
39
|
+
|
40
|
+
# @param request_id [String]
|
41
|
+
def perform(request_id)
|
42
|
+
loop do
|
43
|
+
@mutex.synchronize do
|
44
|
+
track_request(request_id)
|
45
|
+
end
|
46
|
+
|
47
|
+
sleep(TRACK_SLEEP)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Perform an exec request
|
52
|
+
def exec_request
|
53
|
+
Diffend::Voting.call(
|
54
|
+
Diffend::Commands::EXEC,
|
55
|
+
@config,
|
56
|
+
Diffend::BuildBundlerDefinition.call(
|
57
|
+
Diffend::Commands::EXEC,
|
58
|
+
Bundler.default_gemfile,
|
59
|
+
Bundler.default_lockfile
|
60
|
+
)
|
61
|
+
)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Perform a track request
|
65
|
+
#
|
66
|
+
# @param request_id [String]
|
67
|
+
def track_request(request_id)
|
68
|
+
Diffend::Request.call(
|
69
|
+
build_request_object(request_id)
|
70
|
+
)
|
71
|
+
end
|
72
|
+
|
73
|
+
# @param request_id [String]
|
74
|
+
#
|
75
|
+
# @return [Diffend::RequestObject]
|
76
|
+
def build_request_object(request_id)
|
77
|
+
Diffend::RequestObject.new(
|
78
|
+
config: @config,
|
79
|
+
url: track_url(@config.project_id, request_id),
|
80
|
+
payload: { id: request_id }.freeze,
|
81
|
+
request_method: :put
|
82
|
+
).freeze
|
83
|
+
end
|
84
|
+
|
85
|
+
# Fetch diffend config file
|
86
|
+
#
|
87
|
+
# @return [OpenStruct, nil] configuration object
|
88
|
+
#
|
89
|
+
# @raise [Errors::MissingConfigurationFile] when no config file
|
90
|
+
def fetch_config
|
91
|
+
Config::Fetcher.call(
|
92
|
+
File.expand_path('..', Bundler.bin_path)
|
93
|
+
)
|
94
|
+
end
|
95
|
+
|
96
|
+
# @param project_id [String] diffend project_id
|
97
|
+
# @param request_id [String]
|
98
|
+
#
|
99
|
+
# @return [String]
|
100
|
+
def track_url(project_id, request_id)
|
101
|
+
"https://my.diffend.io/api/projects/#{project_id}/bundle/#{request_id}/track"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Diffend
|
4
|
+
# Verifies voting verdicts for gems
|
5
|
+
module Voting
|
6
|
+
class << self
|
7
|
+
# Build verdict
|
8
|
+
#
|
9
|
+
# @param command [String] either install or update
|
10
|
+
# @param config [OpenStruct] diffend config
|
11
|
+
# @param definition [Bundler::Definition] definition for your source
|
12
|
+
def call(command, config, definition)
|
13
|
+
Versions::Remote
|
14
|
+
.call(command, config, definition)
|
15
|
+
.tap { |response| build_message(command, config, response) }
|
16
|
+
end
|
17
|
+
|
18
|
+
# @param command [String] either install or update
|
19
|
+
# @param config [OpenStruct] diffend config
|
20
|
+
# @param response [Hash] response from diffend API
|
21
|
+
def build_message(command, config, response)
|
22
|
+
if response.key?('error')
|
23
|
+
build_error(response)
|
24
|
+
elsif response.key?('action')
|
25
|
+
build_verdict(command, config, response)
|
26
|
+
else
|
27
|
+
Diffend::HandleErrors::Report.call(
|
28
|
+
config: config,
|
29
|
+
message: :unsupported_response,
|
30
|
+
payload: response,
|
31
|
+
report: true
|
32
|
+
)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# @param response [Hash] response from diffend API
|
37
|
+
def build_error(response)
|
38
|
+
build_error_message(response)
|
39
|
+
.tap(&Bundler.ui.method(:error))
|
40
|
+
|
41
|
+
raise Diffend::Errors::HandledException
|
42
|
+
end
|
43
|
+
|
44
|
+
# @param command [String] either install or update
|
45
|
+
# @param config [OpenStruct] diffend config
|
46
|
+
# @param response [Hash] response from diffend API
|
47
|
+
def build_verdict(command, config, response)
|
48
|
+
case response['action']
|
49
|
+
when 'allow'
|
50
|
+
build_allow_message(command, response)
|
51
|
+
.tap(&Bundler.ui.method(:confirm))
|
52
|
+
when 'warn'
|
53
|
+
build_warn_message(command, response)
|
54
|
+
.tap(&Bundler.ui.method(:warn))
|
55
|
+
when 'deny'
|
56
|
+
build_deny_message(command, response)
|
57
|
+
.tap(&Bundler.ui.method(:error))
|
58
|
+
|
59
|
+
exit 1
|
60
|
+
else
|
61
|
+
Diffend::HandleErrors::Report.call(
|
62
|
+
config: config,
|
63
|
+
message: :unsupported_verdict,
|
64
|
+
payload: response,
|
65
|
+
report: true
|
66
|
+
)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# @param response [Hash] response from diffend API
|
71
|
+
#
|
72
|
+
# @return [String]
|
73
|
+
def build_error_message(response)
|
74
|
+
<<~MSG
|
75
|
+
\nDiffend returned an error for your request.\n
|
76
|
+
#{response['error']}\n
|
77
|
+
MSG
|
78
|
+
end
|
79
|
+
|
80
|
+
# @param command [String] either install or update
|
81
|
+
# @param response [Hash] response from diffend API
|
82
|
+
#
|
83
|
+
# @return [String]
|
84
|
+
def build_allow_message(command, response)
|
85
|
+
<<~MSG
|
86
|
+
#{build_message_header('an allow', command)}
|
87
|
+
#{build_message_info(response)}\n
|
88
|
+
#{response['review_url']}\n
|
89
|
+
MSG
|
90
|
+
end
|
91
|
+
|
92
|
+
# @param command [String] either install or update
|
93
|
+
# @param response [Hash] response from diffend API
|
94
|
+
#
|
95
|
+
# @return [String]
|
96
|
+
def build_warn_message(command, response)
|
97
|
+
<<~MSG
|
98
|
+
#{build_message_header('a warn', command)}
|
99
|
+
#{build_message_info(response)} Please go to the url below and review the issues.\n
|
100
|
+
#{response['review_url']}\n
|
101
|
+
MSG
|
102
|
+
end
|
103
|
+
|
104
|
+
# @param command [String] either install or update
|
105
|
+
# @param response [Hash] response from diffend API
|
106
|
+
#
|
107
|
+
# @return [String]
|
108
|
+
def build_deny_message(command, response)
|
109
|
+
<<~MSG
|
110
|
+
#{build_message_header('a deny', command)}
|
111
|
+
#{build_message_info(response)} Please go to the url below and review the issues.\n
|
112
|
+
#{response['review_url']}\n
|
113
|
+
MSG
|
114
|
+
end
|
115
|
+
|
116
|
+
# @param type [String] verdict type
|
117
|
+
# @param command [String] either install or update
|
118
|
+
#
|
119
|
+
# @return [String]
|
120
|
+
def build_message_header(type, command)
|
121
|
+
"\nDiffend reported #{type} verdict for #{command} command for this project."
|
122
|
+
end
|
123
|
+
|
124
|
+
# @param response [Hash] response from diffend API
|
125
|
+
#
|
126
|
+
# @return [String]
|
127
|
+
def build_message_info(response)
|
128
|
+
"\nQuality score: #{response['quality_score']}, allows: #{response['allows_count']}, warnings: #{response['warns_count']}, denies: #{response['denies_count']}."
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,304 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Diffend
|
4
|
+
module Voting
|
5
|
+
# Module responsible for handling both local and remote gem versions
|
6
|
+
module Versions
|
7
|
+
# Module responsible for preparing current or current/new versions of gems
|
8
|
+
class Local
|
9
|
+
# Definition of a local path, if it matches it means that we are the source
|
10
|
+
ME_PATH = '.'
|
11
|
+
# Sources that we expect to match ourselves too
|
12
|
+
ME_SOURCES = [
|
13
|
+
Bundler::Source::Gemspec,
|
14
|
+
Bundler::Source::Path
|
15
|
+
].freeze
|
16
|
+
# List of dependency types
|
17
|
+
DEPENDENCIES_TYPES = {
|
18
|
+
direct: 0,
|
19
|
+
dependency: 1
|
20
|
+
}.freeze
|
21
|
+
# List of sources types
|
22
|
+
SOURCES_TYPES = {
|
23
|
+
valid: 0,
|
24
|
+
multiple_primary: 1
|
25
|
+
}.freeze
|
26
|
+
# List of gem sources types
|
27
|
+
GEM_SOURCES_TYPES = {
|
28
|
+
local: 0,
|
29
|
+
gemfile_source: 1,
|
30
|
+
gemfile_git: 2,
|
31
|
+
gemfile_path: 3
|
32
|
+
}.freeze
|
33
|
+
|
34
|
+
class << self
|
35
|
+
# @param command [String] either install or update
|
36
|
+
# @param definition [Bundler::Definition] definition for your source
|
37
|
+
def call(command, definition)
|
38
|
+
Bundler.ui.silence { definition.resolve_remotely! }
|
39
|
+
|
40
|
+
instance = new(definition)
|
41
|
+
|
42
|
+
case command
|
43
|
+
when Commands::INSTALL, Commands::EXEC then instance.build_install
|
44
|
+
when Commands::UPDATE then instance.build_update
|
45
|
+
else
|
46
|
+
raise ArgumentError, "invalid command: #{command}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# @param definition [Bundler::Definition] definition for your source
|
52
|
+
#
|
53
|
+
# @return [Hash] local dependencies
|
54
|
+
def initialize(definition)
|
55
|
+
@definition = definition
|
56
|
+
@direct_dependencies = Hash[definition.dependencies.map { |val| [val.name, val] }]
|
57
|
+
# Support case without Gemfile.lock
|
58
|
+
@locked_specs = @definition.locked_gems ? @definition.locked_gems.specs : []
|
59
|
+
end
|
60
|
+
|
61
|
+
# Build install specification
|
62
|
+
#
|
63
|
+
# @return [Hash]
|
64
|
+
def build_install
|
65
|
+
hash = build_main
|
66
|
+
|
67
|
+
@definition.specs.each do |spec|
|
68
|
+
next if skip?(spec.source)
|
69
|
+
|
70
|
+
locked_spec = @locked_specs.find { |s| s.name == spec.name }
|
71
|
+
|
72
|
+
hash['dependencies'][spec.name] = {
|
73
|
+
'platform' => build_spec_platform(spec, locked_spec),
|
74
|
+
'source' => build_spec_source(spec),
|
75
|
+
'type' => build_dependency_type(spec.name),
|
76
|
+
'versions' => build_versions(spec, locked_spec)
|
77
|
+
}
|
78
|
+
end
|
79
|
+
|
80
|
+
hash
|
81
|
+
end
|
82
|
+
|
83
|
+
# Build update specification
|
84
|
+
#
|
85
|
+
# @return [Hash]
|
86
|
+
def build_update
|
87
|
+
hash = build_main
|
88
|
+
|
89
|
+
@definition.specs.each do |spec|
|
90
|
+
next if skip?(spec.source)
|
91
|
+
|
92
|
+
locked_spec = @locked_specs.find { |s| s.name == spec.name }
|
93
|
+
|
94
|
+
hash['dependencies'][spec.name] = {
|
95
|
+
'platform' => build_spec_platform(spec, locked_spec),
|
96
|
+
'source' => build_spec_source(spec),
|
97
|
+
'type' => build_dependency_type(spec.name),
|
98
|
+
'versions' => build_versions(spec, locked_spec)
|
99
|
+
}
|
100
|
+
end
|
101
|
+
|
102
|
+
hash
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
# Build default specification
|
108
|
+
#
|
109
|
+
# @return [Hash]
|
110
|
+
def build_main
|
111
|
+
{
|
112
|
+
'dependencies' => {},
|
113
|
+
'sources' => build_sources,
|
114
|
+
'plugins' => {},
|
115
|
+
'platforms' => @definition.platforms.map(&:to_s)
|
116
|
+
}
|
117
|
+
end
|
118
|
+
|
119
|
+
# Build gem versions
|
120
|
+
#
|
121
|
+
# @param spec [Bundler::StubSpecification, Bundler::LazySpecification, Gem::Specification]
|
122
|
+
# @param locked_spec [Bundler::LazySpecification, Gem::Specification, NilClass]
|
123
|
+
#
|
124
|
+
# @return [Array<String>]
|
125
|
+
def build_versions(spec, locked_spec = nil)
|
126
|
+
if locked_spec && locked_spec.version.to_s != spec.version.to_s
|
127
|
+
[locked_spec.version.to_s, spec.version.to_s]
|
128
|
+
else
|
129
|
+
[spec.version.to_s]
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# @param specs [Array] specs that are direct dependencies
|
134
|
+
# @param name [String] spec name
|
135
|
+
#
|
136
|
+
# @return [Boolean] dependency type
|
137
|
+
def build_dependency_type(name)
|
138
|
+
if @direct_dependencies.key?(name)
|
139
|
+
DEPENDENCIES_TYPES[:direct]
|
140
|
+
else
|
141
|
+
DEPENDENCIES_TYPES[:dependency]
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Build gem platform
|
146
|
+
#
|
147
|
+
# @param spec [Bundler::StubSpecification, Bundler::LazySpecification, Gem::Specification]
|
148
|
+
# @param locked_spec [Bundler::LazySpecification, Gem::Specification, NilClass]
|
149
|
+
#
|
150
|
+
# @return [String]
|
151
|
+
def build_spec_platform(spec, locked_spec)
|
152
|
+
parse_platform(
|
153
|
+
spec.platform || locked_spec&.platform || spec.send(:generic_local_platform)
|
154
|
+
)
|
155
|
+
end
|
156
|
+
|
157
|
+
# Parse gem platform
|
158
|
+
#
|
159
|
+
# @param platform [String, Gem::Platform]
|
160
|
+
#
|
161
|
+
# @return [String]
|
162
|
+
def parse_platform(platform)
|
163
|
+
case platform
|
164
|
+
when String then platform
|
165
|
+
when Gem::Platform then platform.os
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# Build gem source type
|
170
|
+
#
|
171
|
+
# @param source [Bundler::Source] gem source type
|
172
|
+
#
|
173
|
+
# @return [Integer] internal gem source type
|
174
|
+
def build_spec_gem_source_type(source)
|
175
|
+
case source
|
176
|
+
when Bundler::Source::Metadata
|
177
|
+
GEM_SOURCES_TYPES[:local]
|
178
|
+
when Bundler::Source::Rubygems, Bundler::Source::Rubygems::Remote
|
179
|
+
GEM_SOURCES_TYPES[:gemfile_source]
|
180
|
+
when Bundler::Source::Git
|
181
|
+
GEM_SOURCES_TYPES[:gemfile_git]
|
182
|
+
when Bundler::Source::Path
|
183
|
+
GEM_SOURCES_TYPES[:gemfile_path]
|
184
|
+
else
|
185
|
+
raise ArgumentError, "unknown source #{source.class}"
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
# Build gem source
|
190
|
+
#
|
191
|
+
# @param spec [Bundler::StubSpecification, Bundler::LazySpecification, Gem::Specification]
|
192
|
+
#
|
193
|
+
# @return [Hash]
|
194
|
+
def build_spec_source(spec)
|
195
|
+
source = source_for_spec(spec)
|
196
|
+
|
197
|
+
{
|
198
|
+
'type' => build_spec_gem_source_type(source),
|
199
|
+
'value' => source_name_from_source(source)
|
200
|
+
}
|
201
|
+
end
|
202
|
+
|
203
|
+
# Figure out source for gem
|
204
|
+
#
|
205
|
+
# @param spec [Bundler::StubSpecification, Bundler::LazySpecification, Gem::Specification]
|
206
|
+
#
|
207
|
+
# @return [Bundler::Source] gem source type
|
208
|
+
def source_for_spec(spec)
|
209
|
+
return spec.remote if spec.remote
|
210
|
+
|
211
|
+
case spec.source
|
212
|
+
when Bundler::Source::Rubygems
|
213
|
+
spec
|
214
|
+
.source
|
215
|
+
.send(:remote_specs)
|
216
|
+
.search(Bundler::Dependency.new(spec.name, spec.version))
|
217
|
+
.last
|
218
|
+
.remote
|
219
|
+
when Bundler::Source::Metadata, Bundler::Source::Git, Bundler::Source::Path
|
220
|
+
spec.source
|
221
|
+
else
|
222
|
+
raise ArgumentError, "unknown source #{spec.source.class}"
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
# Build gem source name
|
227
|
+
#
|
228
|
+
# @param source [Bundler::Source] gem source type
|
229
|
+
#
|
230
|
+
# @return [String]
|
231
|
+
def source_name_from_source(source)
|
232
|
+
case source
|
233
|
+
when Bundler::Source::Metadata
|
234
|
+
''
|
235
|
+
when Bundler::Source::Rubygems::Remote
|
236
|
+
source_name(source.anonymized_uri)
|
237
|
+
when Bundler::Source::Git
|
238
|
+
source.instance_variable_get(:@safe_uri)
|
239
|
+
when Bundler::Source::Path
|
240
|
+
source.path
|
241
|
+
else
|
242
|
+
raise ArgumentError, "unknown source #{source.class}"
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
# @param uri [Bundler::URI]
|
247
|
+
#
|
248
|
+
# @return [String]
|
249
|
+
def source_name(uri)
|
250
|
+
uri.to_s[0...-1]
|
251
|
+
end
|
252
|
+
|
253
|
+
# Build sources used in the Gemfile
|
254
|
+
#
|
255
|
+
# @return [Array<Hash>]
|
256
|
+
def build_sources
|
257
|
+
sources = @definition.send(:sources).rubygems_sources
|
258
|
+
hash = {}
|
259
|
+
|
260
|
+
sources.each do |source|
|
261
|
+
type = build_source_type(source.remotes)
|
262
|
+
|
263
|
+
source.remotes.each do |src|
|
264
|
+
hash[source_name(src)] = type
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
hash.map { |name, type| { 'name' => name, 'type' => type } }
|
269
|
+
end
|
270
|
+
|
271
|
+
# Build gem source type
|
272
|
+
#
|
273
|
+
# @param remotes [Array<Bundler::URI>]
|
274
|
+
#
|
275
|
+
# @return [Integer] internal source type
|
276
|
+
def build_source_type(remotes)
|
277
|
+
remotes.count > 1 ? SOURCES_TYPES[:multiple_primary] : SOURCES_TYPES[:valid]
|
278
|
+
end
|
279
|
+
|
280
|
+
# Checks if we should skip a source
|
281
|
+
#
|
282
|
+
# @param source [Bundler::Source] gem source type
|
283
|
+
#
|
284
|
+
# @return [Boolean] true if we should skip this source, false otherwise
|
285
|
+
def skip?(source)
|
286
|
+
return true if me?(source)
|
287
|
+
|
288
|
+
false
|
289
|
+
end
|
290
|
+
|
291
|
+
# Checks if it's a self source, this happens for repositories that are a gem
|
292
|
+
#
|
293
|
+
# @param source [Bundler::Source] gem source type
|
294
|
+
#
|
295
|
+
# @return [Boolean] true if it's a self source, false otherwise
|
296
|
+
def me?(source)
|
297
|
+
return false unless ME_SOURCES.include?(source.class)
|
298
|
+
|
299
|
+
source.path.to_s == ME_PATH
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|