dependabot-common 0.290.0 → 0.292.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: 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