dependabot-common 0.289.0 → 0.291.0
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/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 +7 -6
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
|
-
autorequire:
|
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,8 +615,8 @@ 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
|
-
post_install_message:
|
618
|
+
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.291.0
|
619
|
+
post_install_message:
|
619
620
|
rdoc_options: []
|
620
621
|
require_paths:
|
621
622
|
- lib
|
@@ -631,7 +632,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
631
632
|
version: 3.3.7
|
632
633
|
requirements: []
|
633
634
|
rubygems_version: 3.5.9
|
634
|
-
signing_key:
|
635
|
+
signing_key:
|
635
636
|
specification_version: 4
|
636
637
|
summary: Shared code used across Dependabot Core
|
637
638
|
test_files: []
|