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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: df0add56e9f09e63d8481893c92cb68ff4fa3724271c4378287b22853f237d15
4
- data.tar.gz: bd4edfe5bb0ce0823d6a61fa3ec525ea98b77e1bfd4f665ba043f1f18db06834
3
+ metadata.gz: cebae8e92439e403f480e7ffdcdb009582b7d5e196322fbdab8005048e77b05b
4
+ data.tar.gz: 724f55170bf99cb90ef277776daed5415e6bdb6ed3dd30bde35e849711e7b68f
5
5
  SHA512:
6
- metadata.gz: 193dcab40ec19933c8fb9ca6eea0a41514ba006e4b554083d2ffbaee802e29d572fe0cf1897df3433981f85632aa7d03e700fce06d2b90661afc76630d4e39e9
7
- data.tar.gz: 3c85ae8e5c55f03fca4ee85aa303a79385c188b34c402c5d8675ce65b51487ca43eab21abaa1570eea257964c0f7fbe2d16b0669754d31b441b8a4898f307d50
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.289.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.289.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-05 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,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.289.0
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: []