dependabot-common 0.290.0 → 0.292.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: bc0d7a7acc0f4dcb2e25a622e816fd82a11a1553eecf85e6ae1e442ce5750ffb
4
+ data.tar.gz: 29e3f86968cb122e49a26f2866ee3554cb07ddbbda305e17d02ee4cb10099282
5
5
  SHA512:
6
- metadata.gz: fb9557f02c4bbe7fa7b447862205f49fd04a8c029b7d643163f9707ea3a3c166b1f62fcc190c94a63d03012d271b0010bf7fc8f28b4429c9ed496d79d24c4e64
7
- data.tar.gz: 16c278fbada2c58f30ae4068c05d5b0ba441cac4f655ddee16cc12b862176f714b6b7eceab0e4cd9679b150843fec0725a76b4cd31d613ec8647547fccd0e116
6
+ metadata.gz: 14e6659eaa880f07f1d2562d89ba71a5f581bcf431edcc49983bf7b6819be8567e0bd252606daa744e0c7d3523d2fd1970e22699b1053179f648009517ca332f
7
+ data.tar.gz: 47e39f274165302b4a2da440f242ffb7fd2e635c535666d3c09dc968090fbf7187c614ce1ed642f3ea472093820918675bb7ae465d4cad48a8c644d7ef5d6db8
@@ -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
@@ -32,6 +32,10 @@ module Dependabot
32
32
  normalizer = name_normaliser_for(dependency)
33
33
  dep_name = T.must(normalizer).call(dependency.name)
34
34
 
35
+ if dependency.version.nil? && dependency.requirements.any?
36
+ dependency = extract_base_version_from_requirement(dependency)
37
+ end
38
+
35
39
  @ignore_conditions
36
40
  .select { |ic| self.class.wildcard_match?(T.must(normalizer).call(ic.dependency_name), dep_name) }
37
41
  .map { |ic| ic.ignored_versions(dependency, security_updates_only) }
@@ -40,6 +44,19 @@ module Dependabot
40
44
  .uniq
41
45
  end
42
46
 
47
+ sig { params(dependency: Dependency).returns(Dependency) }
48
+ def extract_base_version_from_requirement(dependency)
49
+ requirements = dependency.requirements
50
+ requirement = T.must(requirements.first)[:requirement]
51
+ version = requirement&.match(/\d+\.\d+\.\d+/)&.to_s
52
+ Dependabot::Dependency.new(
53
+ name: dependency.name,
54
+ version: version,
55
+ requirements: dependency.requirements,
56
+ package_manager: dependency.package_manager
57
+ )
58
+ end
59
+
43
60
  sig { params(wildcard_string: T.nilable(String), candidate_string: T.nilable(String)).returns(T::Boolean) }
44
61
  def self.wildcard_match?(wildcard_string, candidate_string)
45
62
  return false unless wildcard_string && candidate_string
@@ -17,30 +17,38 @@ module Dependabot
17
17
  abstract!
18
18
  # Initialize version information for a package manager or language.
19
19
  # @param name [String] the name of the package manager or language (e.g., "bundler", "ruby").
20
- # @param version [Dependabot::Version] the parsed current version.
20
+ # @param detected_version [Dependabot::Version] the detected version of the package manager or language.
21
+ # @param version [Dependabot::Version] the version dependabots run on.
21
22
  # @param deprecated_versions [Array<Dependabot::Version>] an array of deprecated versions.
22
23
  # @param supported_versions [Array<Dependabot::Version>] an array of supported versions.
23
24
  # @param requirement [Dependabot::Requirement] an array of requirements.
24
25
  # @example
25
- # VersionManager.new("bundler", "2.1.4", nil)
26
+ # VersionManager.new(
27
+ # name: "bundler",
28
+ # version: Version.new("2.1.4"),
29
+ # requirement: nil
30
+ # )
26
31
  sig do
27
32
  params(
28
33
  name: String,
29
- version: Dependabot::Version,
34
+ detected_version: T.nilable(Dependabot::Version),
35
+ version: T.nilable(Dependabot::Version),
30
36
  deprecated_versions: T::Array[Dependabot::Version],
31
37
  supported_versions: T::Array[Dependabot::Version],
32
38
  requirement: T.nilable(Dependabot::Requirement)
33
39
  ).void
34
40
  end
35
41
  def initialize(
36
- name,
37
- version,
38
- deprecated_versions = [],
39
- supported_versions = [],
40
- requirement = nil
42
+ name:,
43
+ detected_version: nil,
44
+ version: nil,
45
+ deprecated_versions: [],
46
+ supported_versions: [],
47
+ requirement: nil
41
48
  )
42
49
  @name = T.let(name, String)
43
- @version = T.let(version, Dependabot::Version)
50
+ @detected_version = T.let(detected_version || version, T.nilable(Dependabot::Version))
51
+ @version = T.let(version, T.nilable(Dependabot::Version))
44
52
  @deprecated_versions = T.let(deprecated_versions, T::Array[Dependabot::Version])
45
53
  @supported_versions = T.let(supported_versions, T::Array[Dependabot::Version])
46
54
  @requirement = T.let(requirement, T.nilable(Dependabot::Requirement))
@@ -52,10 +60,16 @@ module Dependabot
52
60
  sig { returns(String) }
53
61
  attr_reader :name
54
62
 
63
+ # The current version of the package manager or language.
64
+ # @example
65
+ # detected_version #=> Dependabot::Version.new("2")
66
+ sig { returns(T.nilable(Dependabot::Version)) }
67
+ attr_reader :detected_version
68
+
55
69
  # The current version of the package manager or language.
56
70
  # @example
57
71
  # version #=> Dependabot::Version.new("2.1.4")
58
- sig { returns(Dependabot::Version) }
72
+ sig { returns(T.nilable(Dependabot::Version)) }
59
73
  attr_reader :version
60
74
 
61
75
  # Returns an array of deprecated versions of the package manager.
@@ -76,16 +90,34 @@ module Dependabot
76
90
  sig { returns(T.nilable(Dependabot::Requirement)) }
77
91
  attr_reader :requirement
78
92
 
93
+ # The version of the package manager or language as a string.
94
+ # @example
95
+ # version_to_s #=> "2.1"
96
+ sig { returns(String) }
97
+ def version_to_s
98
+ version.to_s
99
+ end
100
+
101
+ # The raw version of the package manager or language.
102
+ # @example
103
+ # raw_version #=> "2.1.4"
104
+ sig { returns(String) }
105
+ def version_to_raw_s
106
+ version&.to_semver.to_s
107
+ end
108
+
79
109
  # Checks if the current version is deprecated.
80
110
  # Returns true if the version is in the deprecated_versions array; false otherwise.
81
111
  # @example
82
112
  # deprecated? #=> true
83
113
  sig { returns(T::Boolean) }
84
114
  def deprecated?
115
+ return false unless detected_version
116
+
85
117
  # If the version is unsupported, the unsupported error is getting raised separately.
86
118
  return false if unsupported?
87
119
 
88
- deprecated_versions.include?(version)
120
+ deprecated_versions.include?(detected_version)
89
121
  end
90
122
 
91
123
  # Checks if the current version is unsupported.
@@ -93,16 +125,20 @@ module Dependabot
93
125
  # unsupported? #=> false
94
126
  sig { returns(T::Boolean) }
95
127
  def unsupported?
128
+ return false unless detected_version
129
+
96
130
  return false if supported_versions.empty?
97
131
 
98
132
  # Check if the version is not supported
99
- supported_versions.all? { |supported| supported > version }
133
+ supported_versions.all? { |supported| supported > detected_version }
100
134
  end
101
135
 
102
136
  # Raises an error if the current package manager or language version is unsupported.
103
137
  # If the version is unsupported, it raises a ToolVersionNotSupported error.
104
138
  sig { void }
105
139
  def raise_if_unsupported!
140
+ return unless detected_version
141
+
106
142
  return unless unsupported?
107
143
 
108
144
  # Example: v2.*, v3.*
@@ -110,7 +146,7 @@ module Dependabot
110
146
 
111
147
  raise ToolVersionNotSupported.new(
112
148
  name,
113
- version.to_s,
149
+ detected_version.to_s,
114
150
  supported_versions_message
115
151
  )
116
152
  end
@@ -83,6 +83,11 @@ module Dependabot
83
83
  # and responsibility for fixing it is on them, not us. As a result we
84
84
  # quietly log these as errors
85
85
  { "error-type": "server_error" }
86
+ when BadRequirementError
87
+ {
88
+ "error-type": "illformed_requirement",
89
+ "error-detail": { message: error.message }
90
+ }
86
91
  when *Octokit::RATE_LIMITED_ERRORS
87
92
  # If we get a rate-limited error we let dependabot-api handle the
88
93
  # retry by re-enqueing the update job after the reset
@@ -144,6 +149,11 @@ module Dependabot
144
149
  "error-type": "git_dependencies_not_reachable",
145
150
  "error-detail": { "dependency-urls": error.dependency_urls }
146
151
  }
152
+ when Dependabot::UnresolvableVersionError
153
+ {
154
+ "error-type": "unresolvable_version",
155
+ "error-detail": { dependencies: error.dependencies }
156
+ }
147
157
  when Dependabot::NotImplemented
148
158
  {
149
159
  "error-type": "not_implemented",
@@ -661,6 +671,23 @@ module Dependabot
661
671
  end
662
672
  end
663
673
 
674
+ class UnresolvableVersionError < DependabotError
675
+ extend T::Sig
676
+
677
+ sig { returns(T::Array[String]) }
678
+ attr_reader :dependencies
679
+
680
+ sig { params(dependencies: T::Array[String]).void }
681
+ def initialize(dependencies)
682
+ @dependencies = dependencies
683
+
684
+ msg = "Unable to determine semantic version from tags or commits for dependencies. " \
685
+ "Dependencies must have a tag or commit that references a semantic version. " \
686
+ "Affected dependencies: #{@dependencies.join(', ')}"
687
+ super(msg)
688
+ end
689
+ end
690
+
664
691
  class GitDependenciesNotReachable < DependabotError
665
692
  extend T::Sig
666
693
 
@@ -311,7 +311,7 @@ module Dependabot
311
311
 
312
312
  SharedHelpers.with_git_configured(credentials: credentials) do
313
313
  Dir.chdir(T.must(repo_contents_path)) do
314
- return SharedHelpers.run_shell_command("git rev-parse HEAD").strip
314
+ return SharedHelpers.run_shell_command("git rev-parse HEAD", stderr_to_stdout: false).strip
315
315
  end
316
316
  end
317
317
  end
@@ -71,15 +71,20 @@ module Dependabot
71
71
  # Generates a description for supported versions.
72
72
  # @param supported_versions [Array<Dependabot::Version>, nil] The supported versions of the package manager.
73
73
  # @param support_later_versions [Boolean] Whether later versions are supported.
74
+ # @param version_manager_type [Symbol] The type of entity being deprecated i.e. :language or :package_manager
74
75
  # @return [String, nil] The generated description or nil if no supported versions are provided.
75
76
  sig do
76
77
  params(
77
78
  supported_versions: T.nilable(T::Array[Dependabot::Version]),
78
- support_later_versions: T::Boolean
79
+ support_later_versions: T::Boolean,
80
+ version_manager_type: Symbol
79
81
  ).returns(String)
80
82
  end
81
- def self.generate_supported_versions_description(supported_versions, support_later_versions)
82
- return "Please upgrade your package manager version" unless supported_versions&.any?
83
+ def self.generate_supported_versions_description(
84
+ supported_versions, support_later_versions, version_manager_type = :package_manager
85
+ )
86
+ entity_text = version_manager_type == :language ? "language" : "package manager"
87
+ return "Please upgrade your #{entity_text} version" unless supported_versions&.any?
83
88
 
84
89
  versions_string = supported_versions.map { |version| "`v#{version}`" }
85
90
 
@@ -94,25 +99,28 @@ module Dependabot
94
99
  "Please upgrade to one of the following versions: #{versions_string}#{later_description}."
95
100
  end
96
101
 
97
- # Generates a deprecation notice for the given package manager.
98
- # @param package_manager [VersionManager] The package manager object.
99
- # @return [Notice, nil] The generated deprecation notice or nil if the package manager is not deprecated.
102
+ # Generates a deprecation notice for the given version manager.
103
+ # @param version_manager [VersionManager] The version manager object.
104
+ # @param version_manager_type [Symbol] The version manager type e.g. :language or :package_manager
105
+ # @return [Notice, nil] The generated deprecation notice or nil if the version manager is not deprecated.
100
106
  sig do
101
107
  params(
102
- package_manager: Ecosystem::VersionManager
108
+ version_manager: Ecosystem::VersionManager,
109
+ version_manager_type: Symbol
103
110
  ).returns(T.nilable(Notice))
104
111
  end
105
- def self.generate_pm_deprecation_notice(package_manager)
106
- return nil unless package_manager.deprecated?
112
+ def self.generate_deprecation_notice(version_manager, version_manager_type = :package_manager)
113
+ return nil unless version_manager.deprecated?
107
114
 
108
115
  mode = NoticeMode::WARN
109
116
  supported_versions_description = generate_supported_versions_description(
110
- package_manager.supported_versions,
111
- package_manager.support_later_versions?
117
+ version_manager.supported_versions,
118
+ version_manager.support_later_versions?,
119
+ version_manager_type
112
120
  )
113
- notice_type = "#{package_manager.name}_deprecated_warn"
114
- title = "Package manager deprecation notice"
115
- description = "Dependabot will stop supporting `#{package_manager.name} v#{package_manager.version}`!"
121
+ notice_type = "#{version_manager.name}_deprecated_warn"
122
+ title = version_manager_type == :language ? "Language deprecation notice" : "Package manager deprecation notice"
123
+ description = "Dependabot will stop supporting `#{version_manager.name} v#{version_manager.detected_version}`!"
116
124
 
117
125
  ## Add the supported versions to the description
118
126
  description += "\n\n#{supported_versions_description}\n" unless supported_versions_description.empty?
@@ -120,7 +128,7 @@ module Dependabot
120
128
  Notice.new(
121
129
  mode: mode,
122
130
  type: notice_type,
123
- package_manager_name: package_manager.name,
131
+ package_manager_name: version_manager.name,
124
132
  title: title,
125
133
  description: description,
126
134
  show_in_pr: true,
@@ -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)
@@ -87,7 +87,7 @@ module Dependabot
87
87
 
88
88
  sig { returns(String) }
89
89
  def head_sha
90
- run_shell_command("git rev-parse HEAD").strip
90
+ run_shell_command("git rev-parse HEAD", stderr_to_stdout: false).strip
91
91
  end
92
92
 
93
93
  sig { returns(String) }
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.292.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.292.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: 2025-01-09 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.292.0
618
619
  post_install_message:
619
620
  rdoc_options: []
620
621
  require_paths:
@@ -630,7 +631,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
630
631
  - !ruby/object:Gem::Version
631
632
  version: 3.3.7
632
633
  requirements: []
633
- rubygems_version: 3.5.9
634
+ rubygems_version: 3.5.22
634
635
  signing_key:
635
636
  specification_version: 4
636
637
  summary: Shared code used across Dependabot Core