Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type SharedHelpers more thoroughly #8310

Merged
merged 9 commits into from
Nov 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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