Skip to content

Commit

Permalink
Type SharedHelpers more thoroughly (#8310)
Browse files Browse the repository at this point in the history
  • Loading branch information
JamieMagee committed Nov 2, 2023
1 parent c947610 commit f3fec1f
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 26 deletions.
26 changes: 24 additions & 2 deletions bundler/lib/dependabot/bundler/native_helpers.rb
Original file line number Diff line number Diff line change
@@ -1,39 +1,59 @@
# typed: true
# typed: strict
# frozen_string_literal: true

require "bundler"
require "sorbet-runtime"
require "dependabot/shared_helpers"

module Dependabot
module Bundler
module NativeHelpers
extend T::Sig
extend T::Generic

class BundleCommand
extend T::Sig

MAX_SECONDS = 1800
MIN_SECONDS = 60

sig { params(timeout_seconds: T.nilable(Integer)).void }
def initialize(timeout_seconds)
@timeout_seconds = clamp(timeout_seconds)
@timeout_seconds = T.let(clamp(timeout_seconds), Integer)
end

sig { params(script: String).returns(String) }
def build(script)
[timeout_command, :ruby, script].compact.join(" ")
end

private

sig { returns(Integer) }
attr_reader :timeout_seconds

sig { returns(T.nilable(String)) }
def timeout_command
"timeout -s HUP #{timeout_seconds}" unless timeout_seconds.zero?
end

sig { params(seconds: T.nilable(Integer)).returns(Integer) }
def clamp(seconds)
return 0 unless seconds

seconds.to_i.clamp(MIN_SECONDS, MAX_SECONDS)
end
end

sig do
params(
function: String,
args: T::Hash[Symbol, String],
bundler_version: String,
options: T::Hash[Symbol, T.untyped]
)
.returns(T.untyped)
end
def self.run_bundler_subprocess(function:, args:, bundler_version:, options: {})
# Run helper suprocess with all bundler-related ENV variables removed
helpers_path = versioned_helper_path(bundler_version)
Expand All @@ -60,10 +80,12 @@ def self.run_bundler_subprocess(function:, args:, bundler_version:, options: {})
end
end

sig { params(bundler_major_version: String).returns(String) }
def self.versioned_helper_path(bundler_major_version)
File.join(native_helpers_root, "v#{bundler_major_version}")
end

sig { returns(String) }
def self.native_helpers_root
helpers_root = ENV.fetch("DEPENDABOT_NATIVE_HELPERS_PATH", nil)
return File.join(helpers_root, "bundler") unless helpers_root.nil?
Expand Down
12 changes: 6 additions & 6 deletions bundler/spec/dependabot/bundler/native_helpers_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
with_env("DEPENDABOT_NATIVE_HELPERS_PATH", native_helpers_path) do
subject.run_bundler_subprocess(
function: "noop",
args: [],
args: {},
bundler_version: "2",
options: options
)
Expand All @@ -34,7 +34,7 @@
.with(
command: "timeout -s HUP 120 ruby /opt/bundler/v2/run.rb",
function: "noop",
args: [],
args: {},
env: anything
)
end
Expand All @@ -54,7 +54,7 @@
.with(
command: "timeout -s HUP 1800 ruby /opt/bundler/v2/run.rb",
function: "noop",
args: [],
args: {},
env: anything
)
end
Expand All @@ -74,7 +74,7 @@
.with(
command: "timeout -s HUP 60 ruby /opt/bundler/v2/run.rb",
function: "noop",
args: [],
args: {},
env: anything
)
end
Expand All @@ -89,7 +89,7 @@
.with(
command: "ruby /opt/bundler/v2/run.rb",
function: "noop",
args: [],
args: {},
env: anything
)
end
Expand All @@ -104,7 +104,7 @@
.with(
command: "ruby #{File.expand_path('../../../helpers/v2/run.rb', __dir__)}",
function: "noop",
args: [],
args: {},
env: anything
)
end
Expand Down
107 changes: 93 additions & 14 deletions common/lib/dependabot/shared_helpers.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# typed: true
# typed: strict
# frozen_string_literal: true

require "digest"
Expand All @@ -21,13 +21,25 @@ module Dependabot
module SharedHelpers
extend T::Sig

GIT_CONFIG_GLOBAL_PATH = File.expand_path(".gitconfig", Utils::BUMP_TMP_DIR_PATH)
USER_AGENT = "dependabot-core/#{Dependabot::VERSION} " \
"#{Excon::USER_AGENT} ruby/#{RUBY_VERSION} " \
"(#{RUBY_PLATFORM}) " \
"(+https://github.com/dependabot/dependabot-core)".freeze
GIT_CONFIG_GLOBAL_PATH = T.let(File.expand_path(".gitconfig", Utils::BUMP_TMP_DIR_PATH), String)
USER_AGENT = T.let(
"dependabot-core/#{Dependabot::VERSION} " \
"#{Excon::USER_AGENT} ruby/#{RUBY_VERSION} " \
"(#{RUBY_PLATFORM}) " \
"(+https://github.com/dependabot/dependabot-core)".freeze,
String
)
SIGKILL = 9

sig do
type_parameters(:T)
.params(
directory: String,
repo_contents_path: T.nilable(String),
block: T.proc.params(arg0: T.any(Pathname, String)).returns(T.type_parameter(:T))
)
.returns(T.type_parameter(:T))
end
def self.in_a_temporary_repo_directory(directory = "/", repo_contents_path = nil, &block)
if repo_contents_path
# If a workspace has been defined to allow orcestration of the git repo
Expand All @@ -49,9 +61,18 @@ def self.in_a_temporary_repo_directory(directory = "/", repo_contents_path = nil
end
end

def self.in_a_temporary_directory(directory = "/")
sig do
type_parameters(:T)
.params(
directory: String,
_block: T.proc.params(arg0: T.any(Pathname, String)).returns(T.type_parameter(:T))
)
.returns(T.type_parameter(:T))
end
def self.in_a_temporary_directory(directory = "/", &_block)
FileUtils.mkdir_p(Utils::BUMP_TMP_DIR_PATH)
tmp_dir = Dir.mktmpdir(Utils::BUMP_TMP_FILE_PREFIX, Utils::BUMP_TMP_DIR_PATH)
path = Pathname.new(File.join(tmp_dir, directory)).expand_path

begin
path = Pathname.new(File.join(tmp_dir, directory)).expand_path
Expand All @@ -63,29 +84,59 @@ def self.in_a_temporary_directory(directory = "/")
end

class HelperSubprocessFailed < Dependabot::DependabotError
attr_reader :error_class, :error_context, :trace
extend T::Sig

sig { returns(String) }
attr_reader :error_class

sig { returns(T::Hash[Symbol, String]) }
attr_reader :error_context

sig { returns(T.nilable(T::Array[String])) }
attr_reader :trace

sig do
params(
message: String,
error_context: T::Hash[Symbol, String],
error_class: T.nilable(String),
trace: T.nilable(T::Array[String])
).void
end
def initialize(message:, error_context:, error_class: nil, trace: nil)
super(message)
@error_class = error_class || "HelperSubprocessFailed"
@error_class = T.let(error_class || "HelperSubprocessFailed", String)
@error_context = error_context
@fingerprint = error_context[:fingerprint] || error_context[:command]
@fingerprint = T.let(error_context[:fingerprint] || error_context[:command], T.nilable(String))
@trace = trace
end

sig { returns(T::Hash[Symbol, T.untyped]) }
def raven_context
{ fingerprint: [@fingerprint], extra: @error_context.except(:stderr_output, :fingerprint) }
end
end

# Escapes all special characters, e.g. = & | <>
sig { params(command: String).returns(String) }
def self.escape_command(command)
command_parts = command.split.map(&:strip).reject(&:empty?)
Shellwords.join(command_parts)
end

# rubocop:disable Metrics/MethodLength
# rubocop:disable Metrics/AbcSize
sig do
params(
command: String,
function: String,
args: T.any(T::Array[String], T::Hash[Symbol, String]),
env: T.nilable(T::Hash[String, String]),
stderr_to_stdout: T::Boolean,
allow_unsafe_shell_command: T::Boolean
)
.returns(T.nilable(T.any(String, T::Hash[String, T.untyped], T::Array[T::Hash[String, T.untyped]])))
end
def self.run_helper_subprocess(command:, function:, args:, env: nil,
stderr_to_stdout: false,
allow_unsafe_shell_command: false)
Expand Down Expand Up @@ -150,6 +201,7 @@ def self.run_helper_subprocess(command:, function:, args:, env: nil,
end

# rubocop:enable Metrics/MethodLength
sig { params(stderr: T.nilable(String), error_context: T::Hash[Symbol, String]).void }
def self.check_out_of_memory_error(stderr, error_context)
return unless stderr&.include?("JavaScript heap out of memory")

Expand All @@ -160,22 +212,25 @@ def self.check_out_of_memory_error(stderr, error_context)
)
end

sig { returns(T::Array[T.class_of(Excon::Middleware::Base)]) }
def self.excon_middleware
Excon.defaults[:middlewares] +
T.must(T.cast(Excon.defaults, T::Hash[Symbol, T::Array[T.class_of(Excon::Middleware::Base)]])[:middlewares]) +
[Excon::Middleware::Decompress] +
[Excon::Middleware::RedirectFollower]
end

sig { params(headers: T.nilable(T::Hash[String, String])).returns(T::Hash[String, String]) }
def self.excon_headers(headers = nil)
headers ||= {}
{
"User-Agent" => USER_AGENT
}.merge(headers)
end

sig { params(options: T.nilable(T::Hash[Symbol, T.untyped])).returns(T::Hash[Symbol, T.untyped]) }
def self.excon_defaults(options = nil)
options ||= {}
headers = options.delete(:headers)
headers = T.cast(options.delete(:headers), T.nilable(T::Hash[String, String]))
{
instrumentor: Dependabot::SimpleInstrumentor,
connect_timeout: 5,
Expand All @@ -188,7 +243,15 @@ def self.excon_defaults(options = nil)
}.merge(options)
end

def self.with_git_configured(credentials:)
sig do
type_parameters(:T)
.params(
credentials: T::Array[T::Hash[String, String]],
_block: T.proc.returns(T.type_parameter(:T))
)
.returns(T.type_parameter(:T))
end
def self.with_git_configured(credentials:, &_block)
safe_directories = find_safe_directories

FileUtils.mkdir_p(Utils::BUMP_TMP_DIR_PATH)
Expand All @@ -209,17 +272,20 @@ def self.with_git_configured(credentials:)
end

# Handle SCP-style git URIs
sig { params(uri: String).returns(String) }
def self.scp_to_standard(uri)
return uri unless uri.start_with?("git@")

"https://#{uri.split('git@').last.sub(%r{:/?}, '/')}"
"https://#{T.must(uri.split('git@').last).sub(%r{:/?}, '/')}"
end

sig { returns(String) }
def self.credential_helper_path
File.join(__dir__, "../../bin/git-credential-store-immutable")
end

# rubocop:disable Metrics/PerceivedComplexity
sig { params(credentials: T::Array[T::Hash[String, String]], safe_directories: T::Array[String]).void }
def self.configure_git_to_use_https_with_credentials(credentials, safe_directories)
File.open(GIT_CONFIG_GLOBAL_PATH, "w") do |file|
file << "# Generated by dependabot/dependabot-core"
Expand Down Expand Up @@ -279,6 +345,7 @@ def self.configure_git_to_use_https_with_credentials(credentials, safe_directori
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/PerceivedComplexity

sig { params(host: String).void }
def self.configure_git_to_use_https(host)
# NOTE: we use --global here (rather than --system) so that Dependabot
# can be run without privileged access
Expand All @@ -304,13 +371,15 @@ def self.configure_git_to_use_https(host)
)
end

sig { params(path: String).void }
def self.reset_git_repo(path)
Dir.chdir(path) do
run_shell_command("git reset HEAD --hard")
run_shell_command("git clean -fx")
end
end

sig { returns(T::Array[String]) }
def self.find_safe_directories
# to preserve safe directories from global .gitconfig
output, process = Open3.capture2("git config --global --get-all safe.directory")
Expand All @@ -319,6 +388,15 @@ def self.find_safe_directories
safe_directories
end

sig do
params(
command: String,
allow_unsafe_shell_command: T::Boolean,
env: T.nilable(T::Hash[String, String]),
fingerprint: T.nilable(String),
stderr_to_stdout: T::Boolean
).returns(String)
end
def self.run_shell_command(command,
allow_unsafe_shell_command: false,
env: {},
Expand Down Expand Up @@ -352,6 +430,7 @@ def self.run_shell_command(command,
)
end

sig { params(command: String, stdin_data: String, env: T.nilable(T::Hash[String, String])).returns(String) }
def self.helper_subprocess_bash_command(command:, stdin_data:, env:)
escaped_stdin_data = stdin_data.gsub("\"", "\\\"")
env_keys = env ? env.compact.map { |k, v| "#{k}=#{v}" }.join(" ") + " " : ""
Expand Down
10 changes: 8 additions & 2 deletions common/lib/dependabot/workspace/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ module Workspace
class Base
extend T::Sig
extend T::Helpers
extend T::Generic

abstract!

sig { returns(T::Array[Dependabot::Workspace::ChangeAttempt]) }
Expand Down Expand Up @@ -38,8 +40,12 @@ def failed_change_attempts
end

sig do
params(memo: T.nilable(String), _blk: T.proc.params(arg0: T.any(Pathname, String)).returns(T.untyped))
.returns(T.untyped)
type_parameters(:T)
.params(
memo: T.nilable(String),
_blk: T.proc.params(arg0: T.any(Pathname, String)).returns(T.type_parameter(:T))
)
.returns(T.type_parameter(:T))
end
def change(memo = nil, &_blk)
Dir.chdir(path) { yield(path) }
Expand Down
Loading

0 comments on commit f3fec1f

Please sign in to comment.