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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4d33a6634dcde9e6c86689c31358cf2ee6c0cfc12ba8e7668940b3fd33710348
4
- data.tar.gz: c472e1fe6402e2300d12be7664c2d89433cf6b7e3f103d25a1cd82e52c03965b
3
+ metadata.gz: cebae8e92439e403f480e7ffdcdb009582b7d5e196322fbdab8005048e77b05b
4
+ data.tar.gz: 724f55170bf99cb90ef277776daed5415e6bdb6ed3dd30bde35e849711e7b68f
5
5
  SHA512:
6
- metadata.gz: fb9557f02c4bbe7fa7b447862205f49fd04a8c029b7d643163f9707ea3a3c166b1f62fcc190c94a63d03012d271b0010bf7fc8f28b4429c9ed496d79d24c4e64
7
- data.tar.gz: 16c278fbada2c58f30ae4068c05d5b0ba441cac4f655ddee16cc12b862176f714b6b7eceab0e4cd9679b150843fec0725a76b4cd31d613ec8647547fccd0e116
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
@@ -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
- command_parts = command.split.map(&:strip).reject(&:empty?)
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
- stdout, stderr, process = T.unsafe(Open3).capture3(*env_cmd, stdin_data: stdin_data)
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 ? stderr[0..50_000] : "", # Truncate to ~100kb
189
+ stderr_output: stderr[0..50_000], # Truncate to ~100kb
181
190
  process_exit_value: process.to_s,
182
- process_termsig: process.termsig
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.success?
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
- if stderr_to_stdout
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.success?
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
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Dependabot
5
- VERSION = "0.290.0"
5
+ VERSION = "0.291.0"
6
6
  end
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.290.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-12 00:00:00.000000000 Z
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.290.0
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: