Skip to content

Commit 04ab85a

Browse files
authored
Add API integration to fetch blocked versions at job updates (#15123)
* Add API integration to fetch and handle blocked versions
1 parent 1d785ff commit 04ab85a

8 files changed

Lines changed: 343 additions & 45 deletions

File tree

bin/dry-run.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@
228228

229229
unless ENV["BLOCKED_VERSIONS"].to_s.strip.empty?
230230
# For example:
231-
# [{"dependency-name":"event-stream","version":"= 3.3.6","reason":"malware"}]
231+
# [{"dependency-name":"event-stream","version-requirement":"= 3.3.6","reason":"malware"}]
232232
$options[:blocked_versions] = JSON.parse(ENV.fetch("BLOCKED_VERSIONS", nil))
233233
end
234234

@@ -730,9 +730,9 @@ def ignored_versions_for(dep)
730730

731731
def blocked_versions_for(dep)
732732
$options[:blocked_versions]
733-
.select { |bv| bv["dependency-name"] && bv["version"] }
733+
.select { |bv| bv["dependency-name"] && bv["version-requirement"] }
734734
.select { |bv| bv["dependency-name"].casecmp(dep.name).zero? }
735-
.map { |bv| bv["version"] }
735+
.map { |bv| bv["version-requirement"] }
736736
end
737737

738738
def security_advisories
@@ -794,7 +794,7 @@ def security_fix?(dependency)
794794
if $options[:blocked_versions].any?
795795
puts "=> blocked versions active:"
796796
$options[:blocked_versions].each do |bv|
797-
msg = " #{bv['dependency-name']} #{bv['version']}"
797+
msg = " #{bv['dependency-name']} #{bv['version-requirement']}"
798798
msg += " (#{bv['reason']})" if bv["reason"]
799799
puts msg
800800
end

updater/lib/dependabot/api_client.rb

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,39 @@ 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+
parsed = JSON.parse(response.body.to_s)
406+
unless parsed.is_a?(Hash)
407+
Dependabot.logger.warn("Unexpected blocked versions format, continuing without them")
408+
return []
409+
end
410+
data = parsed.fetch("data", [])
411+
unless data.is_a?(Array) && data.all?(Hash)
412+
Dependabot.logger.warn("Unexpected blocked versions format, continuing without them")
413+
return []
414+
end
415+
data
416+
rescue JSON::ParserError, TypeError => e
417+
Dependabot.logger.warn("Failed to parse blocked versions response: #{e.message}, continuing without them")
418+
[]
419+
rescue Excon::Error::Socket, Excon::Error::Timeout, OpenSSL::SSL::SSLError => e
420+
Dependabot.logger.warn("Failed to fetch blocked versions: #{e.message}, continuing without them")
421+
[]
422+
end
423+
end
424+
392425
private
393426

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

updater/lib/dependabot/job.rb

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,9 @@ class Job # rubocop:disable Metrics/ClassLength
122122
sig { returns(T::Array[T::Hash[String, T.untyped]]) }
123123
attr_reader :blocked_versions
124124

125+
sig { params(blocked_versions: T::Array[T::Hash[String, T.untyped]]).void }
126+
attr_writer :blocked_versions
127+
125128
sig { returns(Dependabot::Config::UpdateConfig) }
126129
attr_reader :update_config
127130

@@ -491,25 +494,25 @@ def log_ignore_conditions_for(dependency)
491494
sig { params(dependency: Dependabot::Dependency).returns(T::Array[String]) }
492495
def blocked_versions_for(dependency)
493496
matching_blocked_entries(dependency).filter_map do |bv|
494-
version = bv["version"].strip
495-
version.empty? ? nil : version
497+
req = bv["version-requirement"].strip
498+
req.empty? ? nil : req
496499
end
497500
end
498501

499502
sig { params(dependency: Dependabot::Dependency).void }
500503
def log_blocked_versions_for(dependency)
501504
entries = matching_blocked_entries(dependency).filter_map do |bv|
502-
version = bv["version"].strip
503-
next if version.empty?
505+
req = bv["version-requirement"].strip
506+
next if req.empty?
504507

505508
reason = bv["reason"].is_a?(String) ? bv["reason"].strip : nil
506-
{ version: version, reason: reason&.empty? ? nil : reason }
509+
{ version_requirement: req, reason: reason&.empty? ? nil : reason }
507510
end
508511
return if entries.empty?
509512

510513
Dependabot.logger.info("Blocked versions (by GitHub Security):")
511514
entries.each do |entry|
512-
msg = " #{entry[:version]}"
515+
msg = " #{entry[:version_requirement]}"
513516
msg += " - reason: #{entry[:reason]}" if entry[:reason]
514517
Dependabot.logger.info(msg)
515518
end
@@ -524,7 +527,7 @@ def matching_blocked_entries(dependency)
524527
normalized_dep_name = T.must(normaliser).call(dependency.name)
525528

526529
blocked_versions
527-
.select { |bv| bv["dependency-name"].is_a?(String) && bv["version"].is_a?(String) }
530+
.select { |bv| bv["dependency-name"].is_a?(String) && bv["version-requirement"].is_a?(String) }
528531
.select { |bv| T.must(normaliser).call(bv["dependency-name"]) == normalized_dep_name }
529532
end
530533

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: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,20 @@ def perform_job
6969
sig { override.returns(Dependabot::Job) }
7070
def job
7171
@job ||= T.let(
72-
Job.new_update_job(
73-
job_id: job_id,
74-
job_definition: Environment.job_definition,
75-
repo_contents_path: Environment.repo_contents_path
76-
),
72+
begin
73+
update_job = Job.new_update_job(
74+
job_id: job_id,
75+
job_definition: Environment.job_definition,
76+
repo_contents_path: Environment.repo_contents_path
77+
)
78+
79+
if Experiments.enabled?(:blocked_versions)
80+
blocked = service.fetch_blocked_versions(update_job.package_manager)
81+
update_job.blocked_versions = blocked
82+
end
83+
84+
update_job
85+
end,
7786
T.nilable(Dependabot::Job)
7887
)
7988
end

updater/spec/dependabot/api_client_spec.rb

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -800,4 +800,144 @@
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-requirement" => "= 3.3.6", "reason" => "malware" },
816+
{ "dependency-name" => "flatmap-stream", "version-requirement" => "= 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-requirement" => "= 3.3.6", "reason" => "malware" },
828+
{ "dependency-name" => "flatmap-stream", "version-requirement" => "= 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+
879+
context "when the API returns invalid JSON" do
880+
before do
881+
stub_request(:get, blocked_versions_url)
882+
.with(query: { "package-manager": "npm_and_yarn" })
883+
.to_return(status: 200, body: "not json", headers: headers)
884+
end
885+
886+
it "returns an empty array and logs a warning" do
887+
expect(Dependabot.logger).to receive(:warn).with(/Failed to parse blocked versions/)
888+
result = client.fetch_blocked_versions("npm_and_yarn")
889+
expect(result).to eq([])
890+
end
891+
end
892+
893+
context "when the API returns data that is not an array" do
894+
before do
895+
stub_request(:get, blocked_versions_url)
896+
.with(query: { "package-manager": "npm_and_yarn" })
897+
.to_return(
898+
status: 200,
899+
body: { data: "unexpected" }.to_json,
900+
headers: headers
901+
)
902+
end
903+
904+
it "returns an empty array and logs a warning" do
905+
expect(Dependabot.logger).to receive(:warn).with(/Unexpected blocked versions format/)
906+
result = client.fetch_blocked_versions("npm_and_yarn")
907+
expect(result).to eq([])
908+
end
909+
end
910+
911+
context "when the API returns a non-object JSON body" do
912+
before do
913+
stub_request(:get, blocked_versions_url)
914+
.with(query: { "package-manager": "npm_and_yarn" })
915+
.to_return(status: 200, body: "[]", headers: headers)
916+
end
917+
918+
it "returns an empty array and logs a warning" do
919+
expect(Dependabot.logger).to receive(:warn).with(/Unexpected blocked versions format/)
920+
result = client.fetch_blocked_versions("npm_and_yarn")
921+
expect(result).to eq([])
922+
end
923+
end
924+
925+
context "when the API returns data entries that are not hashes" do
926+
before do
927+
stub_request(:get, blocked_versions_url)
928+
.with(query: { "package-manager": "npm_and_yarn" })
929+
.to_return(
930+
status: 200,
931+
body: { data: [1, "not-a-hash"] }.to_json,
932+
headers: headers
933+
)
934+
end
935+
936+
it "returns an empty array and logs a warning" do
937+
expect(Dependabot.logger).to receive(:warn).with(/Unexpected blocked versions format/)
938+
result = client.fetch_blocked_versions("npm_and_yarn")
939+
expect(result).to eq([])
940+
end
941+
end
942+
end
803943
end

0 commit comments

Comments
 (0)