dependabot-common 0.290.0 → 0.291.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/dependabot/command_helpers.rb +226 -0
- data/lib/dependabot/errors.rb +22 -0
- data/lib/dependabot/shared_helpers.rb +35 -15
- data/lib/dependabot.rb +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cebae8e92439e403f480e7ffdcdb009582b7d5e196322fbdab8005048e77b05b
|
4
|
+
data.tar.gz: 724f55170bf99cb90ef277776daed5415e6bdb6ed3dd30bde35e849711e7b68f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 127292cb53f8677d645cd9a51e89d16babcdacf8d5455c89e442c3022e728597142426ec88362e81082dbe8825ab5e4639f0f98600c644a7294ab9551d329931
|
7
|
+
data.tar.gz: ff5478b081ce3e05babd84dbc2f6730260dd73e676c093655d7c5577f6e45c0b9db9d9da328c6fef5b485e4446f8e2da6d21db7d270692ac6f57e11051c8bd02
|
@@ -0,0 +1,226 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "open3"
|
5
|
+
require "timeout"
|
6
|
+
require "sorbet-runtime"
|
7
|
+
require "shellwords"
|
8
|
+
|
9
|
+
module Dependabot
|
10
|
+
module CommandHelpers
|
11
|
+
extend T::Sig
|
12
|
+
|
13
|
+
module TIMEOUTS
|
14
|
+
NO_TIME_OUT = -1 # No timeout
|
15
|
+
LOCAL = 30 # 30 seconds
|
16
|
+
NETWORK = 120 # 2 minutes
|
17
|
+
LONG_RUNNING = 300 # 5 minutes
|
18
|
+
DEFAULT = 900 # 15 minutes
|
19
|
+
end
|
20
|
+
|
21
|
+
class ProcessStatus
|
22
|
+
extend T::Sig
|
23
|
+
|
24
|
+
sig { params(process_status: Process::Status, custom_exitstatus: T.nilable(Integer)).void }
|
25
|
+
def initialize(process_status, custom_exitstatus = nil)
|
26
|
+
@process_status = process_status
|
27
|
+
@custom_exitstatus = custom_exitstatus
|
28
|
+
end
|
29
|
+
|
30
|
+
# Return the exit status, either from the process status or the custom one
|
31
|
+
sig { returns(Integer) }
|
32
|
+
def exitstatus
|
33
|
+
@custom_exitstatus || @process_status.exitstatus || 0
|
34
|
+
end
|
35
|
+
|
36
|
+
# Determine if the process was successful
|
37
|
+
sig { returns(T::Boolean) }
|
38
|
+
def success?
|
39
|
+
@custom_exitstatus.nil? ? @process_status.success? || false : @custom_exitstatus.zero?
|
40
|
+
end
|
41
|
+
|
42
|
+
# Return the PID of the process (if available)
|
43
|
+
sig { returns(T.nilable(Integer)) }
|
44
|
+
def pid
|
45
|
+
@process_status.pid
|
46
|
+
end
|
47
|
+
|
48
|
+
sig { returns(T.nilable(Integer)) }
|
49
|
+
def termsig
|
50
|
+
@process_status.termsig
|
51
|
+
end
|
52
|
+
|
53
|
+
# String representation of the status
|
54
|
+
sig { returns(String) }
|
55
|
+
def to_s
|
56
|
+
if @custom_exitstatus
|
57
|
+
"pid #{pid || 'unknown'}: exit #{@custom_exitstatus} (custom status)"
|
58
|
+
else
|
59
|
+
@process_status.to_s
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# rubocop:disable Metrics/AbcSize
|
65
|
+
# rubocop:disable Metrics/MethodLength
|
66
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
67
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
68
|
+
sig do
|
69
|
+
params(
|
70
|
+
env_cmd: T::Array[T.any(T::Hash[String, String], String)],
|
71
|
+
stdin_data: T.nilable(String),
|
72
|
+
stderr_to_stdout: T::Boolean,
|
73
|
+
timeout: Integer
|
74
|
+
).returns([T.nilable(String), T.nilable(String), T.nilable(ProcessStatus), Float])
|
75
|
+
end
|
76
|
+
def self.capture3_with_timeout(
|
77
|
+
env_cmd,
|
78
|
+
stdin_data: nil,
|
79
|
+
stderr_to_stdout: false,
|
80
|
+
timeout: TIMEOUTS::DEFAULT
|
81
|
+
)
|
82
|
+
|
83
|
+
stdout = T.let("", String)
|
84
|
+
stderr = T.let("", String)
|
85
|
+
status = T.let(nil, T.nilable(ProcessStatus))
|
86
|
+
pid = T.let(nil, T.untyped)
|
87
|
+
start_time = Time.now
|
88
|
+
|
89
|
+
begin
|
90
|
+
T.unsafe(Open3).popen3(*env_cmd) do |stdin, stdout_io, stderr_io, wait_thr| # rubocop:disable Metrics/BlockLength
|
91
|
+
pid = wait_thr.pid
|
92
|
+
Dependabot.logger.info("Started process PID: #{pid} with command: #{env_cmd.join(' ')}")
|
93
|
+
|
94
|
+
# Write to stdin if input data is provided
|
95
|
+
stdin&.write(stdin_data) if stdin_data
|
96
|
+
stdin&.close
|
97
|
+
|
98
|
+
stdout_io.sync = true
|
99
|
+
stderr_io.sync = true
|
100
|
+
|
101
|
+
# Array to monitor both stdout and stderr
|
102
|
+
ios = [stdout_io, stderr_io]
|
103
|
+
|
104
|
+
last_output_time = Time.now # Track the last time output was received
|
105
|
+
|
106
|
+
until ios.empty?
|
107
|
+
if timeout.positive?
|
108
|
+
# Calculate remaining timeout dynamically
|
109
|
+
remaining_timeout = timeout - (Time.now - last_output_time)
|
110
|
+
|
111
|
+
# Raise an error if timeout is exceeded
|
112
|
+
if remaining_timeout <= 0
|
113
|
+
Dependabot.logger.warn("Process PID: #{pid} timed out after #{timeout}s. Terminating...")
|
114
|
+
terminate_process(pid)
|
115
|
+
status = ProcessStatus.new(wait_thr.value, 124)
|
116
|
+
raise Timeout::Error, "Timed out due to inactivity after #{timeout} seconds"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Use IO.select with a dynamically calculated short timeout
|
121
|
+
ready_ios = IO.select(ios, nil, nil, 0)
|
122
|
+
|
123
|
+
# Process ready IO streams
|
124
|
+
ready_ios&.first&.each do |io|
|
125
|
+
# 1. Read data from the stream
|
126
|
+
io.set_encoding("BINARY")
|
127
|
+
data = io.read_nonblock(1024)
|
128
|
+
|
129
|
+
# 2. Force encoding to UTF-8 (for proper conversion)
|
130
|
+
data.force_encoding("UTF-8")
|
131
|
+
|
132
|
+
# 3. Convert to UTF-8 safely, handling invalid/undefined bytes
|
133
|
+
data = data.encode("UTF-8", invalid: :replace, undef: :replace, replace: "?")
|
134
|
+
|
135
|
+
# Reset the timeout if data is received
|
136
|
+
last_output_time = Time.now unless data.empty?
|
137
|
+
|
138
|
+
# 4. Append data to the appropriate stream
|
139
|
+
if io == stdout_io
|
140
|
+
stdout += data
|
141
|
+
else
|
142
|
+
stderr += data unless stderr_to_stdout
|
143
|
+
stdout += data if stderr_to_stdout
|
144
|
+
end
|
145
|
+
rescue EOFError
|
146
|
+
# Remove the stream when EOF is reached
|
147
|
+
ios.delete(io)
|
148
|
+
rescue IO::WaitReadable
|
149
|
+
# Continue when IO is not ready yet
|
150
|
+
next
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
status = ProcessStatus.new(wait_thr.value)
|
155
|
+
Dependabot.logger.info("Process PID: #{pid} completed with status: #{status}")
|
156
|
+
end
|
157
|
+
rescue Timeout::Error => e
|
158
|
+
Dependabot.logger.error("Process PID: #{pid} failed due to timeout: #{e.message}")
|
159
|
+
terminate_process(pid)
|
160
|
+
|
161
|
+
# Append timeout message only to stderr without interfering with stdout
|
162
|
+
stderr += "\n#{e.message}" unless stderr_to_stdout
|
163
|
+
stdout += "\n#{e.message}" if stderr_to_stdout
|
164
|
+
rescue Errno::ENOENT => e
|
165
|
+
Dependabot.logger.error("Command failed: #{e.message}")
|
166
|
+
stderr += e.message unless stderr_to_stdout
|
167
|
+
stdout += e.message if stderr_to_stdout
|
168
|
+
end
|
169
|
+
|
170
|
+
elapsed_time = Time.now - start_time
|
171
|
+
Dependabot.logger.info("Total execution time: #{elapsed_time.round(2)} seconds")
|
172
|
+
[stdout, stderr, status, elapsed_time]
|
173
|
+
end
|
174
|
+
# rubocop:enable Metrics/AbcSize
|
175
|
+
# rubocop:enable Metrics/MethodLength
|
176
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
177
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
178
|
+
|
179
|
+
# Terminate a process by PID
|
180
|
+
sig { params(pid: T.nilable(Integer)).void }
|
181
|
+
def self.terminate_process(pid)
|
182
|
+
return unless pid
|
183
|
+
|
184
|
+
begin
|
185
|
+
if process_alive?(pid)
|
186
|
+
Process.kill("TERM", pid) # Attempt graceful termination
|
187
|
+
sleep(0.5) # Allow process to terminate
|
188
|
+
end
|
189
|
+
if process_alive?(pid)
|
190
|
+
Process.kill("KILL", pid) # Forcefully kill if still running
|
191
|
+
end
|
192
|
+
rescue Errno::EPERM
|
193
|
+
Dependabot.logger.error("Insufficient permissions to terminate process: #{pid}")
|
194
|
+
ensure
|
195
|
+
begin
|
196
|
+
Process.waitpid(pid)
|
197
|
+
rescue Errno::ESRCH, Errno::ECHILD
|
198
|
+
# Process has already exited
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
# Check if the process is still alive
|
204
|
+
sig { params(pid: T.nilable(Integer)).returns(T::Boolean) }
|
205
|
+
def self.process_alive?(pid)
|
206
|
+
return false if pid.nil?
|
207
|
+
|
208
|
+
begin
|
209
|
+
Process.kill(0, pid) # Check if the process exists
|
210
|
+
true
|
211
|
+
rescue Errno::ESRCH
|
212
|
+
false
|
213
|
+
rescue Errno::EPERM
|
214
|
+
Dependabot.logger.error("Insufficient permissions to check process: #{pid}")
|
215
|
+
false
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
# Escape shell commands to ensure safe execution
|
220
|
+
sig { params(command: String).returns(String) }
|
221
|
+
def self.escape_command(command)
|
222
|
+
command_parts = command.split.map(&:strip).reject(&:empty?)
|
223
|
+
Shellwords.join(command_parts)
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
data/lib/dependabot/errors.rb
CHANGED
@@ -144,6 +144,11 @@ module Dependabot
|
|
144
144
|
"error-type": "git_dependencies_not_reachable",
|
145
145
|
"error-detail": { "dependency-urls": error.dependency_urls }
|
146
146
|
}
|
147
|
+
when Dependabot::UnresolvableVersionError
|
148
|
+
{
|
149
|
+
"error-type": "unresolvable_version",
|
150
|
+
"error-detail": { dependencies: error.dependencies }
|
151
|
+
}
|
147
152
|
when Dependabot::NotImplemented
|
148
153
|
{
|
149
154
|
"error-type": "not_implemented",
|
@@ -661,6 +666,23 @@ module Dependabot
|
|
661
666
|
end
|
662
667
|
end
|
663
668
|
|
669
|
+
class UnresolvableVersionError < DependabotError
|
670
|
+
extend T::Sig
|
671
|
+
|
672
|
+
sig { returns(T::Array[String]) }
|
673
|
+
attr_reader :dependencies
|
674
|
+
|
675
|
+
sig { params(dependencies: T::Array[String]).void }
|
676
|
+
def initialize(dependencies)
|
677
|
+
@dependencies = dependencies
|
678
|
+
|
679
|
+
msg = "Unable to determine semantic version from tags or commits for dependencies. " \
|
680
|
+
"Dependencies must have a tag or commit that references a semantic version. " \
|
681
|
+
"Affected dependencies: #{@dependencies.join(', ')}"
|
682
|
+
super(msg)
|
683
|
+
end
|
684
|
+
end
|
685
|
+
|
664
686
|
class GitDependenciesNotReachable < DependabotError
|
665
687
|
extend T::Sig
|
666
688
|
|
@@ -7,7 +7,6 @@ require "excon"
|
|
7
7
|
require "fileutils"
|
8
8
|
require "json"
|
9
9
|
require "open3"
|
10
|
-
require "shellwords"
|
11
10
|
require "sorbet-runtime"
|
12
11
|
require "tmpdir"
|
13
12
|
|
@@ -17,9 +16,10 @@ require "dependabot/utils"
|
|
17
16
|
require "dependabot/errors"
|
18
17
|
require "dependabot/workspace"
|
19
18
|
require "dependabot"
|
19
|
+
require "dependabot/command_helpers"
|
20
20
|
|
21
21
|
module Dependabot
|
22
|
-
module SharedHelpers
|
22
|
+
module SharedHelpers # rubocop:disable Metrics/ModuleLength
|
23
23
|
extend T::Sig
|
24
24
|
|
25
25
|
GIT_CONFIG_GLOBAL_PATH = T.let(File.expand_path(".gitconfig", Utils::BUMP_TMP_DIR_PATH), String)
|
@@ -121,8 +121,7 @@ module Dependabot
|
|
121
121
|
# Escapes all special characters, e.g. = & | <>
|
122
122
|
sig { params(command: String).returns(String) }
|
123
123
|
def self.escape_command(command)
|
124
|
-
|
125
|
-
Shellwords.join(command_parts)
|
124
|
+
CommandHelpers.escape_command(command)
|
126
125
|
end
|
127
126
|
|
128
127
|
# rubocop:disable Metrics/MethodLength
|
@@ -135,14 +134,16 @@ module Dependabot
|
|
135
134
|
env: T.nilable(T::Hash[String, String]),
|
136
135
|
stderr_to_stdout: T::Boolean,
|
137
136
|
allow_unsafe_shell_command: T::Boolean,
|
138
|
-
error_class: T.class_of(HelperSubprocessFailed)
|
137
|
+
error_class: T.class_of(HelperSubprocessFailed),
|
138
|
+
timeout: Integer
|
139
139
|
)
|
140
140
|
.returns(T.nilable(T.any(String, T::Hash[String, T.untyped], T::Array[T::Hash[String, T.untyped]])))
|
141
141
|
end
|
142
142
|
def self.run_helper_subprocess(command:, function:, args:, env: nil,
|
143
143
|
stderr_to_stdout: false,
|
144
144
|
allow_unsafe_shell_command: false,
|
145
|
-
error_class: HelperSubprocessFailed
|
145
|
+
error_class: HelperSubprocessFailed,
|
146
|
+
timeout: CommandHelpers::TIMEOUTS::DEFAULT)
|
146
147
|
start = Time.now
|
147
148
|
stdin_data = JSON.dump(function: function, args: args)
|
148
149
|
cmd = allow_unsafe_shell_command ? command : escape_command(command)
|
@@ -157,7 +158,15 @@ module Dependabot
|
|
157
158
|
end
|
158
159
|
|
159
160
|
env_cmd = [env, cmd].compact
|
160
|
-
|
161
|
+
if Experiments.enabled?(:enable_shared_helpers_command_timeout)
|
162
|
+
stdout, stderr, process = CommandHelpers.capture3_with_timeout(
|
163
|
+
env_cmd,
|
164
|
+
stdin_data: stdin_data,
|
165
|
+
timeout: timeout
|
166
|
+
)
|
167
|
+
else
|
168
|
+
stdout, stderr, process = T.unsafe(Open3).capture3(*env_cmd, stdin_data: stdin_data)
|
169
|
+
end
|
161
170
|
time_taken = Time.now - start
|
162
171
|
|
163
172
|
if ENV["DEBUG_HELPERS"] == "true"
|
@@ -177,16 +186,16 @@ module Dependabot
|
|
177
186
|
function: function,
|
178
187
|
args: args,
|
179
188
|
time_taken: time_taken,
|
180
|
-
stderr_output: stderr
|
189
|
+
stderr_output: stderr[0..50_000], # Truncate to ~100kb
|
181
190
|
process_exit_value: process.to_s,
|
182
|
-
process_termsig: process
|
191
|
+
process_termsig: process&.termsig
|
183
192
|
}
|
184
193
|
|
185
194
|
check_out_of_memory_error(stderr, error_context, error_class)
|
186
195
|
|
187
196
|
begin
|
188
197
|
response = JSON.parse(stdout)
|
189
|
-
return response["result"] if process
|
198
|
+
return response["result"] if process&.success?
|
190
199
|
|
191
200
|
raise error_class.new(
|
192
201
|
message: response["error"],
|
@@ -415,6 +424,7 @@ module Dependabot
|
|
415
424
|
safe_directories
|
416
425
|
end
|
417
426
|
|
427
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
418
428
|
sig do
|
419
429
|
params(
|
420
430
|
command: String,
|
@@ -422,7 +432,8 @@ module Dependabot
|
|
422
432
|
cwd: T.nilable(String),
|
423
433
|
env: T.nilable(T::Hash[String, String]),
|
424
434
|
fingerprint: T.nilable(String),
|
425
|
-
stderr_to_stdout: T::Boolean
|
435
|
+
stderr_to_stdout: T::Boolean,
|
436
|
+
timeout: Integer
|
426
437
|
).returns(String)
|
427
438
|
end
|
428
439
|
def self.run_shell_command(command,
|
@@ -430,7 +441,8 @@ module Dependabot
|
|
430
441
|
cwd: nil,
|
431
442
|
env: {},
|
432
443
|
fingerprint: nil,
|
433
|
-
stderr_to_stdout: true
|
444
|
+
stderr_to_stdout: true,
|
445
|
+
timeout: CommandHelpers::TIMEOUTS::DEFAULT)
|
434
446
|
start = Time.now
|
435
447
|
cmd = allow_unsafe_shell_command ? command : escape_command(command)
|
436
448
|
|
@@ -439,7 +451,14 @@ module Dependabot
|
|
439
451
|
opts = {}
|
440
452
|
opts[:chdir] = cwd if cwd
|
441
453
|
|
442
|
-
|
454
|
+
env_cmd = [env || {}, cmd, opts].compact
|
455
|
+
if Experiments.enabled?(:enable_shared_helpers_command_timeout)
|
456
|
+
stdout, stderr, process = CommandHelpers.capture3_with_timeout(
|
457
|
+
env_cmd,
|
458
|
+
stderr_to_stdout: stderr_to_stdout,
|
459
|
+
timeout: timeout
|
460
|
+
)
|
461
|
+
elsif stderr_to_stdout
|
443
462
|
stdout, process = Open3.capture2e(env || {}, cmd, opts)
|
444
463
|
else
|
445
464
|
stdout, stderr, process = Open3.capture3(env || {}, cmd, opts)
|
@@ -449,7 +468,7 @@ module Dependabot
|
|
449
468
|
|
450
469
|
# Raise an error with the output from the shell session if the
|
451
470
|
# command returns a non-zero status
|
452
|
-
return stdout if process
|
471
|
+
return stdout || "" if process&.success?
|
453
472
|
|
454
473
|
error_context = {
|
455
474
|
command: cmd,
|
@@ -461,10 +480,11 @@ module Dependabot
|
|
461
480
|
check_out_of_disk_memory_error(stderr, error_context)
|
462
481
|
|
463
482
|
raise SharedHelpers::HelperSubprocessFailed.new(
|
464
|
-
message: stderr_to_stdout ? stdout : "#{stderr}\n#{stdout}",
|
483
|
+
message: stderr_to_stdout ? (stdout || "") : "#{stderr}\n#{stdout}",
|
465
484
|
error_context: error_context
|
466
485
|
)
|
467
486
|
end
|
487
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
468
488
|
|
469
489
|
sig { params(stderr: T.nilable(String), error_context: T::Hash[Symbol, String]).void }
|
470
490
|
def self.check_out_of_disk_memory_error(stderr, error_context)
|
data/lib/dependabot.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dependabot-common
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.291.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dependabot
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-12-
|
11
|
+
date: 2024-12-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: aws-sdk-codecommit
|
@@ -531,6 +531,7 @@ files:
|
|
531
531
|
- lib/dependabot/clients/codecommit.rb
|
532
532
|
- lib/dependabot/clients/github_with_retries.rb
|
533
533
|
- lib/dependabot/clients/gitlab_with_retries.rb
|
534
|
+
- lib/dependabot/command_helpers.rb
|
534
535
|
- lib/dependabot/config.rb
|
535
536
|
- lib/dependabot/config/file.rb
|
536
537
|
- lib/dependabot/config/file_fetcher.rb
|
@@ -614,7 +615,7 @@ licenses:
|
|
614
615
|
- MIT
|
615
616
|
metadata:
|
616
617
|
bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
|
617
|
-
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.
|
618
|
+
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.291.0
|
618
619
|
post_install_message:
|
619
620
|
rdoc_options: []
|
620
621
|
require_paths:
|