Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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
38 changes: 38 additions & 0 deletions updater/bin/fetch_files.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# typed: strict
# frozen_string_literal: true

$LOAD_PATH.unshift(__dir__ + "/../lib")

$stdout.sync = true

require "dependabot/api_client"
require "dependabot/environment"
require "dependabot/service"
require "dependabot/setup"
require "dependabot/file_fetcher_command"
require "debug" if ENV["DEBUG"]

class UpdaterKilledError < StandardError; end

trap("TERM") do
puts "Received SIGTERM"
error = UpdaterKilledError.new("Updater process killed with SIGTERM")
tags = { "gh.dependabot_api.update_job.id": ENV.fetch("DEPENDABOT_JOB_ID", nil) }

api_client =
Dependabot::ApiClient.new(
Dependabot::Environment.api_url,
Dependabot::Environment.job_id,
Dependabot::Environment.job_token
)
Dependabot::Service.new(client: api_client).capture_exception(error: error, tags: tags)
exit
end

begin
# Clones into DEPENDABOT_REPO_CONTENTS_PATH. When that path is a shared volume the
# update container reuses the clone instead of making its own.
Dependabot::FileFetcherCommand.new.run
rescue Dependabot::RunFailure
exit 1
end
19 changes: 12 additions & 7 deletions updater/bin/run
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@ set -e

command="$1"
if [ -z "$command" ]; then
echo "usage: run [update_files|update_graph]"
echo "usage: run [fetch_files|update_files|update_graph]"
exit 1
fi

# ignore fetch_files command for backward compatibility
if [ "$command" = "fetch_files" ]; then
echo "fetch_files command is no longer used directly"
exit 0
fi
case "$command" in
fetch_files|update_files|update_graph) ;;
*)
echo "usage: run [fetch_files|update_files|update_graph]"
exit 1
;;
esac

bundle exec ruby "bin/${command}.rb"
# exec so the Ruby process' exit code becomes the container's exit code: callers
# chain `run fetch_files && run update_files` and must be able to tell a fetch
# failure from a success.
exec bundle exec ruby "bin/${command}.rb"
12 changes: 7 additions & 5 deletions updater/lib/dependabot/file_fetcher_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@
require "dependabot/opentelemetry"
require "dependabot/updater"
require "dependabot/file_fetcher_command_connectivity"
require "dependabot/file_fetcher_command_local_checkout"
require "octokit"
require "sorbet-runtime"

module Dependabot
class FileFetcherCommand < BaseCommand
extend T::Sig
include FileFetcherCommandConnectivity
include FileFetcherCommandLocalCheckout

# BaseCommand does not implement this method, so we should expose
# the instance variable for error handling to avoid raising a
Expand All @@ -38,8 +40,7 @@ def perform_job # rubocop:disable Metrics/AbcSize
begin
connectivity_check if ENV["ENABLE_CONNECTIVITY_CHECK"] == "1"
normalize_single_directory
validate_target_branch
dependabot_ref_namespace_available?
validate_repository
clone_repo_contents
@base_commit_sha = file_fetcher.commit
raise "base commit SHA not found" unless @base_commit_sha
Expand Down Expand Up @@ -293,7 +294,7 @@ def with_retries(max_retries: 2, &_block)
end
end

sig { void }
sig { override.void }
def dependabot_ref_namespace_available?
dependabot_branch = "dependabot"
begin
Expand All @@ -312,7 +313,7 @@ def dependabot_ref_namespace_available?
end
end

sig { void }
sig { override.void }
def validate_target_branch
return unless job.source.branch

Expand All @@ -337,12 +338,13 @@ def validate_target_branch

sig { void }
def clone_repo_contents
return if local_checkout_only?
return unless job.clone?

file_fetcher.clone_repo_contents
end

sig { returns(T::Boolean) }
sig { override.returns(T::Boolean) }
def already_cloned?
return false unless Environment.repo_contents_path

Expand Down
49 changes: 49 additions & 0 deletions updater/lib/dependabot/file_fetcher_command_local_checkout.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# typed: strong
# frozen_string_literal: true

require "sorbet-runtime"

module Dependabot
module FileFetcherCommandLocalCheckout
extend T::Sig
extend T::Helpers

abstract!

private

sig { abstract.void }
def validate_target_branch; end

sig { abstract.void }
def dependabot_ref_namespace_available?; end

sig { abstract.returns(T::Boolean) }
def already_cloned?; end

sig { void }
def validate_repository
validate_local_checkout
return if local_checkout_only?

validate_target_branch
dependabot_ref_namespace_available?
end

sig { void }
def validate_local_checkout
return unless local_checkout_only?

repo_contents_path = Environment.repo_contents_path
Kernel.raise "DEPENDABOT_REPO_CONTENTS_PATH is not set" if repo_contents_path.to_s.empty?
return if already_cloned?

Kernel.raise "Local repository checkout not found at #{repo_contents_path}"
end

sig { returns(T::Boolean) }
def local_checkout_only?
ENV["DEPENDABOT_LOCAL_CHECKOUT_ONLY"] == "true"
end
end
end
57 changes: 57 additions & 0 deletions updater/spec/dependabot/file_fetcher_command_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,63 @@
expect(dependency_file.content_encoding).to eq("utf-8")
end

context "when only a local checkout may be used" do
let(:repo_contents_path) { Dir.mktmpdir }

before do
allow(Dependabot::Environment).to receive_messages(
repo_contents_path: repo_contents_path
)
stub_const("ENV", ENV.to_h.merge("DEPENDABOT_LOCAL_CHECKOUT_ONLY" => "true"))
end

after { FileUtils.rm_rf(repo_contents_path) unless repo_contents_path.to_s.empty? }

it "reports an error instead of fetching the repository when the checkout is missing" do
expect(Dependabot::Bundler::FileFetcher).not_to receive(:new)
expect(api_client).to receive(:record_update_job_error).with(
hash_including(
error_type: "file_fetcher_error",
error_details: hash_including(
Dependabot::ErrorAttributes::MESSAGE => "Local repository checkout not found at #{repo_contents_path}"
)
)
)
expect(api_client).to receive(:mark_job_as_processed)

expect { perform_job }.to output(/Local repository checkout not found/).to_stdout_from_any_process
end

shared_examples "a missing repository contents path" do
it "reports that the repository contents path is not set" do
expect(Dependabot::Bundler::FileFetcher).not_to receive(:new)
expect(api_client).to receive(:record_update_job_error).with(
hash_including(
error_type: "file_fetcher_error",
error_details: hash_including(
Dependabot::ErrorAttributes::MESSAGE => "DEPENDABOT_REPO_CONTENTS_PATH is not set"
)
)
)
expect(api_client).to receive(:mark_job_as_processed)

expect { perform_job }.to output(/DEPENDABOT_REPO_CONTENTS_PATH is not set/).to_stdout_from_any_process
end
end

context "when the repository contents path is nil" do
let(:repo_contents_path) { nil }

it_behaves_like "a missing repository contents path"
end

context "when the repository contents path is empty" do
let(:repo_contents_path) { "" }

it_behaves_like "a missing repository contents path"
end
end

context "when empty directories are specified" do
before do
allow(Dependabot::Environment).to receive(:repo_contents_path).and_return(Dir.mktmpdir)
Expand Down
Loading