diff --git a/updater/lib/dependabot/file_fetcher_command.rb b/updater/lib/dependabot/file_fetcher_command.rb index 4c544f5d41a..5a178ee01e7 100644 --- a/updater/lib/dependabot/file_fetcher_command.rb +++ b/updater/lib/dependabot/file_fetcher_command.rb @@ -54,6 +54,7 @@ def perform_job # rubocop:disable Metrics/AbcSize Dependabot.logger.error("Error during file fetching; aborting: #{e.message}") end handle_file_fetcher_error(e) + handle_missing_directory_for_pr_update(e) service.mark_job_as_processed(@base_commit_sha) return nil end @@ -408,5 +409,24 @@ def git_metadata_fetcher T.nilable(Dependabot::GitMetadataFetcher) ) end + + # When updating a pull request, if the directory or dependency files + # are no longer found, we should close the PR as the dependency has + # been removed from the project. + sig { params(error: StandardError).void } + def handle_missing_directory_for_pr_update(error) + dependencies = job.dependencies + + return unless job.updating_a_pull_request? + return unless dependencies + return unless error.is_a?(Dependabot::DependencyFileNotFound) || + error.is_a?(Dependabot::DirectoryNotFound) + + Dependabot.logger.info( + "Closing pull request for #{dependencies.join(', ')} " \ + "as the directory or dependency files are no longer present" + ) + service.close_pull_request(dependencies, :dependency_removed) + end end end diff --git a/updater/spec/dependabot/file_fetcher_command_spec.rb b/updater/spec/dependabot/file_fetcher_command_spec.rb index b9f75e40d5d..2cddefcc213 100644 --- a/updater/spec/dependabot/file_fetcher_command_spec.rb +++ b/updater/spec/dependabot/file_fetcher_command_spec.rb @@ -652,5 +652,141 @@ expect(root_files.map(&:name)).to include("a.dummy") end end + + context "when updating a pull request and directory is not found" do + let(:job_definition) do + job_def = JSON.parse(fixture("jobs/job_with_credentials.json")) + job_def["job"]["updating-a-pull-request"] = true + job_def["job"]["dependencies"] = ["dummy-pkg-a"] + job_def + end + + before do + allow_any_instance_of(Dependabot::Bundler::FileFetcher) + .to receive(:commit) + .and_return("a" * 40) + allow_any_instance_of(Dependabot::Bundler::FileFetcher) + .to receive(:files) + .and_raise(Dependabot::DependencyFileNotFound.new("/some/deleted/directory")) + end + + it "closes the pull request and records the error" do + expect(api_client) + .to receive(:record_update_job_error) + .with( + error_details: { + "file-path": "/some/deleted/directory", + message: "/some/deleted/directory not found" + }, + error_type: "dependency_file_not_found" + ) + expect(api_client) + .to receive(:close_pull_request) + .with(["dummy-pkg-a"], :dependency_removed) + expect(api_client).to receive(:mark_job_as_processed) + + expect { perform_job }.to output(/Error during file fetching; aborting/).to_stdout_from_any_process + end + end + + context "when updating a pull request and DirectoryNotFound is raised" do + let(:job_definition) do + job_def = JSON.parse(fixture("jobs/job_with_credentials.json")) + job_def["job"]["updating-a-pull-request"] = true + job_def["job"]["dependencies"] = ["dummy-pkg-b"] + job_def + end + + before do + allow_any_instance_of(Dependabot::Bundler::FileFetcher) + .to receive(:commit) + .and_return("b" * 40) + allow_any_instance_of(Dependabot::Bundler::FileFetcher) + .to receive(:files) + .and_raise(Dependabot::DirectoryNotFound.new("/deleted/dir")) + end + + it "closes the pull request and records the error" do + expect(api_client) + .to receive(:record_update_job_error) + .with( + error_details: { "directory-name": "/deleted/dir" }, + error_type: "directory_not_found" + ) + expect(api_client) + .to receive(:close_pull_request) + .with(["dummy-pkg-b"], :dependency_removed) + expect(api_client).to receive(:mark_job_as_processed) + + expect { perform_job }.to output(/Error during file fetching; aborting/).to_stdout_from_any_process + end + end + + context "when not updating a pull request and directory is not found" do + let(:job_definition) do + job_def = JSON.parse(fixture("jobs/job_with_credentials.json")) + job_def["job"]["updating-a-pull-request"] = false + job_def + end + + before do + allow_any_instance_of(Dependabot::Bundler::FileFetcher) + .to receive(:commit) + .and_return("c" * 40) + allow_any_instance_of(Dependabot::Bundler::FileFetcher) + .to receive(:files) + .and_raise(Dependabot::DependencyFileNotFound.new("/some/directory")) + end + + it "records the error but does not close any pull request" do + expect(api_client) + .to receive(:record_update_job_error) + .with( + error_details: { + "file-path": "/some/directory", + message: "/some/directory not found" + }, + error_type: "dependency_file_not_found" + ) + expect(api_client).not_to receive(:close_pull_request) + expect(api_client).to receive(:mark_job_as_processed) + + expect { perform_job }.to output(/Error during file fetching; aborting/).to_stdout_from_any_process + end + end + + context "when updating a pull request with nil dependencies and directory is not found" do + let(:job_definition) do + job_def = JSON.parse(fixture("jobs/job_with_credentials.json")) + job_def["job"]["updating-a-pull-request"] = true + job_def["job"]["dependencies"] = nil + job_def + end + + before do + allow_any_instance_of(Dependabot::Bundler::FileFetcher) + .to receive(:commit) + .and_return("d" * 40) + allow_any_instance_of(Dependabot::Bundler::FileFetcher) + .to receive(:files) + .and_raise(Dependabot::DependencyFileNotFound.new("/some/directory")) + end + + it "records the error but does not attempt to close PR" do + expect(api_client) + .to receive(:record_update_job_error) + .with( + error_details: { + "file-path": "/some/directory", + message: "/some/directory not found" + }, + error_type: "dependency_file_not_found" + ) + expect(api_client).not_to receive(:close_pull_request) + expect(api_client).to receive(:mark_job_as_processed) + + expect { perform_job }.to output(/Error during file fetching; aborting/).to_stdout_from_any_process + end + end end end