Skip to content

Commit f682877

Browse files
kbukum1Copilot
andcommitted
Add API integration to fetch blocked versions at job construction
Implement the API call that fetches blocked versions from dependabot-api and injects them into the job definition before the Job object is constructed. Changes: - ApiClient#fetch_blocked_versions: GET endpoint that retrieves blocked versions for a given package manager. Gracefully handles errors (returns [] on failure, logs warning). - Service: delegate fetch_blocked_versions to client - UpdateFilesCommand#job: fetch blocked versions during job construction (gated behind :blocked_versions experiment flag) and inject into job_definition hash - Remove the TODO placeholder from previous PR Tests: - ApiClient spec: success, error, timeout, and empty response cases - UpdateFilesCommand spec: verifies fetch is called with correct package manager and handles empty responses Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent db2d4dd commit f682877

5 files changed

Lines changed: 165 additions & 10 deletions

File tree

updater/lib/dependabot/api_client.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,26 @@ def record_cooldown_meta(job)
389389
# rubocop:enable Metrics/AbcSize
390390
# rubocop:enable Metrics/MethodLength
391391

392+
sig { params(package_manager: String).returns(T::Array[T::Hash[String, T.untyped]]) }
393+
def fetch_blocked_versions(package_manager)
394+
::Dependabot::OpenTelemetry.tracer.in_span("fetch_blocked_versions", kind: :internal) do |span|
395+
span.set_attribute(::Dependabot::OpenTelemetry::Attributes::JOB_ID, job_id.to_s)
396+
397+
api_url = "#{http_client.params[:path]}/update_jobs/#{job_id}/blocked_versions"
398+
response = http_client.get(path: api_url, query: { "package-manager": package_manager })
399+
400+
if response.status >= 400
401+
Dependabot.logger.warn("Failed to fetch blocked versions (HTTP #{response.status}), continuing without them")
402+
return []
403+
end
404+
405+
JSON.parse(response.body).fetch("data", [])
406+
rescue Excon::Error::Socket, Excon::Error::Timeout, OpenSSL::SSL::SSLError => e
407+
Dependabot.logger.warn("Failed to fetch blocked versions: #{e.message}, continuing without them")
408+
[]
409+
end
410+
end
411+
392412
private
393413

394414
# Update return type to allow returning a Hash or nil

updater/lib/dependabot/service.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ def initialize(client:)
4040
:record_ecosystem_versions,
4141
:increment_metric,
4242
:record_ecosystem_meta,
43-
:record_cooldown_meta
43+
:record_cooldown_meta,
44+
:fetch_blocked_versions
4445

4546
sig { void }
4647
def wait_for_calls_to_finish

updater/lib/dependabot/update_files_command.rb

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,6 @@ def perform_job
5050
# As above, we can remove the responsibility for handling fatal/job halting
5151
# errors from Dependabot::Updater entirely.
5252
begin
53-
# TODO: Fetch blocked versions from dependabot-api and inject into the job.
54-
# Call GET /blocked_versions?ecosystem=<job.package_manager> to retrieve
55-
# versions blocked by GitHub Security, then pass them into the job so they
56-
# are excluded from update candidates.
5753
Dependabot::Updater.new(service:, job:, dependency_snapshot:).run
5854
rescue Dependabot::DependencyFileNotParseable => e
5955
handle_dependency_file_not_parseable_error(e)
@@ -73,11 +69,24 @@ def perform_job
7369
sig { override.returns(Dependabot::Job) }
7470
def job
7571
@job ||= T.let(
76-
Job.new_update_job(
77-
job_id: job_id,
78-
job_definition: Environment.job_definition,
79-
repo_contents_path: Environment.repo_contents_path
80-
),
72+
begin
73+
definition = Environment.job_definition
74+
job_hash = definition["job"]
75+
76+
# Fetch blocked versions from the API if the experiment is enabled.
77+
# Inject them into the job definition so they're available at construction time.
78+
if Experiments.enabled?(:blocked_versions)
79+
package_manager = job_hash["package-manager"] || job_hash["package_manager"] || ""
80+
blocked = service.fetch_blocked_versions(package_manager)
81+
job_hash["blocked-versions"] = blocked if blocked.any?
82+
end
83+
84+
Job.new_update_job(
85+
job_id: job_id,
86+
job_definition: definition,
87+
repo_contents_path: Environment.repo_contents_path
88+
)
89+
end,
8190
T.nilable(Dependabot::Job)
8291
)
8392
end

updater/spec/dependabot/api_client_spec.rb

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -800,4 +800,80 @@
800800
end
801801
end
802802
end
803+
804+
describe "fetch_blocked_versions" do
805+
let(:blocked_versions_url) { "http://example.com/update_jobs/1/blocked_versions" }
806+
807+
context "when the API returns blocked versions" do
808+
before do
809+
stub_request(:get, blocked_versions_url)
810+
.with(query: { "package-manager": "npm_and_yarn" })
811+
.to_return(
812+
status: 200,
813+
body: {
814+
data: [
815+
{ "dependency-name" => "event-stream", "version" => "3.3.6", "reason" => "malware" },
816+
{ "dependency-name" => "flatmap-stream", "version" => "0.1.1", "reason" => "malware" }
817+
]
818+
}.to_json,
819+
headers: headers
820+
)
821+
end
822+
823+
it "returns the blocked versions array" do
824+
result = client.fetch_blocked_versions("npm_and_yarn")
825+
expect(result).to eq(
826+
[
827+
{ "dependency-name" => "event-stream", "version" => "3.3.6", "reason" => "malware" },
828+
{ "dependency-name" => "flatmap-stream", "version" => "0.1.1", "reason" => "malware" }
829+
]
830+
)
831+
end
832+
end
833+
834+
context "when the API returns an error" do
835+
before do
836+
stub_request(:get, blocked_versions_url)
837+
.with(query: { "package-manager": "npm_and_yarn" })
838+
.to_return(status: 500, body: "Internal Server Error", headers: headers)
839+
end
840+
841+
it "returns an empty array and logs a warning" do
842+
expect(Dependabot.logger).to receive(:warn).with(/Failed to fetch blocked versions/)
843+
result = client.fetch_blocked_versions("npm_and_yarn")
844+
expect(result).to eq([])
845+
end
846+
end
847+
848+
context "when the API times out" do
849+
before do
850+
stub_request(:get, blocked_versions_url)
851+
.with(query: { "package-manager": "npm_and_yarn" })
852+
.to_timeout
853+
end
854+
855+
it "returns an empty array and logs a warning" do
856+
expect(Dependabot.logger).to receive(:warn).with(/Failed to fetch blocked versions/)
857+
result = client.fetch_blocked_versions("npm_and_yarn")
858+
expect(result).to eq([])
859+
end
860+
end
861+
862+
context "when the API returns no blocked versions" do
863+
before do
864+
stub_request(:get, blocked_versions_url)
865+
.with(query: { "package-manager": "npm_and_yarn" })
866+
.to_return(
867+
status: 200,
868+
body: { data: [] }.to_json,
869+
headers: headers
870+
)
871+
end
872+
873+
it "returns an empty array" do
874+
result = client.fetch_blocked_versions("npm_and_yarn")
875+
expect(result).to eq([])
876+
end
877+
end
878+
end
803879
end

updater/spec/dependabot/update_files_command_spec.rb

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,4 +438,53 @@
438438
end
439439
end
440440
end
441+
442+
describe "#perform_job with blocked versions experiment enabled" do
443+
subject(:perform_job) { job.perform_job }
444+
445+
before do
446+
Dependabot::Experiments.register(:blocked_versions, true)
447+
allow(service).to receive(:fetch_blocked_versions).and_return(blocked_versions)
448+
end
449+
450+
after do
451+
Dependabot::Experiments.reset!
452+
end
453+
454+
let(:blocked_versions) do
455+
[
456+
{ "dependency-name" => "rails", "version" => "7.0.0", "reason" => "vulnerability" }
457+
]
458+
end
459+
460+
it "fetches blocked versions and injects them into the job definition" do
461+
dummy_runner = double(run: nil)
462+
allow(Dependabot::Updater).to receive(:new).and_return(dummy_runner)
463+
allow(dummy_runner).to receive(:run)
464+
allow(service).to receive(:mark_job_as_processed)
465+
allow(service).to receive(:update_dependency_list)
466+
467+
expect(service).to receive(:fetch_blocked_versions).with("bundler")
468+
469+
perform_job
470+
end
471+
472+
context "when the API returns no blocked versions" do
473+
let(:blocked_versions) { [] }
474+
475+
it "does not inject blocked versions into the job definition" do
476+
dummy_runner = double(run: nil)
477+
allow(Dependabot::Updater).to receive(:new).and_return(dummy_runner)
478+
allow(dummy_runner).to receive(:run)
479+
allow(service).to receive(:mark_job_as_processed)
480+
allow(service).to receive(:update_dependency_list)
481+
482+
perform_job
483+
484+
# Job should still have empty blocked_versions (the default)
485+
job_instance = job.send(:job)
486+
expect(job_instance.blocked_versions).to eq([])
487+
end
488+
end
489+
end
441490
end

0 commit comments

Comments
 (0)