Skip to content

Commit b5d414c

Browse files
kbukum1Copilot
andcommitted
Add blocked versions support to updater job
Integrate a blocked_versions job attribute into the updater's ignore logic so that dependency versions flagged by GitHub Security are automatically excluded from update candidates. Changes: - Add blocked_versions to Job PERMITTED_KEYS and constructor - Merge blocked versions into ignore_conditions_for as exact-match version requirements (= <version>) - Use normalized exact name matching (not wildcard) for safety - Add logging for blocked versions in log_ignore_conditions_for - Defensive handling of malformed entries (missing name/version) - Add TODO in update_files_command.rb for API integration point Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 4231fc8 commit b5d414c

3 files changed

Lines changed: 237 additions & 13 deletions

File tree

updater/lib/dependabot/job.rb

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ class Job # rubocop:disable Metrics/ClassLength
5757
cooldown
5858
repo_private
5959
multi_ecosystem_update
60+
blocked_versions
6061
).freeze,
6162
T::Array[Symbol]
6263
)
@@ -118,6 +119,9 @@ class Job # rubocop:disable Metrics/ClassLength
118119
sig { returns(T.nilable(Dependabot::Package::ReleaseCooldownOptions)) }
119120
attr_reader :cooldown
120121

122+
sig { returns(T::Array[T::Hash[String, T.untyped]]) }
123+
attr_reader :blocked_versions
124+
121125
sig { returns(Dependabot::Config::UpdateConfig) }
122126
attr_reader :update_config
123127

@@ -220,6 +224,10 @@ def initialize(attributes) # rubocop:disable Metrics/AbcSize,Metrics/MethodLengt
220224
@dependency_groups = T.let(attributes.fetch(:dependency_groups, []) || [], T::Array[T.untyped])
221225
@dependency_group_to_refresh = T.let(attributes.fetch(:dependency_group_to_refresh, nil), T.nilable(String))
222226
@repo_private = T.let(attributes.fetch(:repo_private, nil), T.nilable(T::Boolean))
227+
@blocked_versions = T.let(
228+
attributes.fetch(:blocked_versions, []) || [],
229+
T::Array[T::Hash[String, T.untyped]]
230+
)
223231

224232
@update_config = T.let(calculate_update_config, Dependabot::Config::UpdateConfig)
225233

@@ -444,7 +452,7 @@ def ignore_conditions_for(dependency)
444452
# allow update-types cannot be checked in allowed_update? because it runs
445453
# pre-resolution when only the current version is known. Version ranges
446454
# only need the current version — the same mechanism as ignore update-types.
447-
conditions + ignored_versions_from_allowed_update_types(dependency)
455+
conditions + ignored_versions_from_allowed_update_types(dependency) + blocked_versions_for(dependency)
448456
end
449457

450458
# TODO: Present Dependabot::Config::IgnoreCondition in calling code
@@ -460,24 +468,55 @@ def ignore_conditions_for(dependency)
460468
sig { params(dependency: Dependabot::Dependency).void }
461469
def log_ignore_conditions_for(dependency)
462470
conditions = ignore_conditions.select { |ic| name_match?(ic["dependency-name"], dependency.name) }
463-
return if conditions.empty?
464-
465-
Dependabot.logger.info("Ignored versions:")
466-
conditions.each do |ic|
467-
unless ic["version-requirement"].nil?
468-
Dependabot.logger.info(" #{ic['version-requirement']} - from #{ic['source']}")
469-
end
470-
471-
ic["update-types"]&.each do |update_type|
472-
msg = " #{update_type} - from #{ic['source']}"
473-
msg += " (doesn't apply to security update)" if security_updates_only?
474-
Dependabot.logger.info(msg)
471+
if conditions.any?
472+
Dependabot.logger.info("Ignored versions:")
473+
conditions.each do |ic|
474+
unless ic["version-requirement"].nil?
475+
Dependabot.logger.info(" #{ic['version-requirement']} - from #{ic['source']}")
476+
end
477+
478+
ic["update-types"]&.each do |update_type|
479+
msg = " #{update_type} - from #{ic['source']}"
480+
msg += " (doesn't apply to security update)" if security_updates_only?
481+
Dependabot.logger.info(msg)
482+
end
475483
end
476484
end
485+
486+
log_blocked_versions_for(dependency)
477487
end
478488

479489
private
480490

491+
sig { params(dependency: Dependabot::Dependency).returns(T::Array[String]) }
492+
def blocked_versions_for(dependency)
493+
normaliser = name_normaliser
494+
normalized_dep_name = T.must(normaliser).call(dependency.name)
495+
496+
blocked_versions
497+
.select { |bv| bv["dependency-name"] && bv["version"] }
498+
.select { |bv| T.must(normaliser).call(bv["dependency-name"]) == normalized_dep_name }
499+
.map { |bv| "= #{bv['version']}" }
500+
end
501+
502+
sig { params(dependency: Dependabot::Dependency).void }
503+
def log_blocked_versions_for(dependency)
504+
normaliser = name_normaliser
505+
normalized_dep_name = T.must(normaliser).call(dependency.name)
506+
507+
blocks = blocked_versions
508+
.select { |bv| bv["dependency-name"] && bv["version"] }
509+
.select { |bv| T.must(normaliser).call(bv["dependency-name"]) == normalized_dep_name }
510+
return if blocks.empty?
511+
512+
Dependabot.logger.info("Blocked versions (by GitHub Security):")
513+
blocks.each do |bv|
514+
msg = " = #{bv['version']}"
515+
msg += " - reason: #{bv['reason']}" if bv["reason"]
516+
Dependabot.logger.info(msg)
517+
end
518+
end
519+
481520
sig { params(dependency: Dependabot::Dependency).returns(T::Boolean) }
482521
def completely_ignored?(dependency)
483522
ignore_conditions_for(dependency).any?(Dependabot::Config::IgnoreCondition::ALL_VERSIONS)

updater/lib/dependabot/update_files_command.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ 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.
5357
Dependabot::Updater.new(service:, job:, dependency_snapshot:).run
5458
rescue Dependabot::DependencyFileNotParseable => e
5559
handle_dependency_file_not_parseable_error(e)

updater/spec/dependabot/job_spec.rb

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1273,4 +1273,185 @@
12731273
end
12741274
end
12751275
end
1276+
1277+
describe "#ignore_conditions_for with blocked_versions" do
1278+
subject(:ignored) { job.ignore_conditions_for(dependency) }
1279+
1280+
let(:dependency) do
1281+
Dependabot::Dependency.new(
1282+
name: "event-stream",
1283+
package_manager: "bundler",
1284+
version: "3.3.5",
1285+
requirements: [{ file: "Gemfile", requirement: "~> 3.3", groups: [], source: nil }]
1286+
)
1287+
end
1288+
1289+
context "when blocked_versions contains a matching dependency" do
1290+
let(:attributes) do
1291+
super().merge(
1292+
blocked_versions: [
1293+
{
1294+
"dependency-name" => "event-stream",
1295+
"version" => "3.3.6",
1296+
"reason" => "malware - flatmap-stream injection"
1297+
}
1298+
]
1299+
)
1300+
end
1301+
1302+
it "includes the blocked version as an exact ignore condition" do
1303+
expect(ignored).to include("= 3.3.6")
1304+
end
1305+
end
1306+
1307+
context "when blocked_versions contains multiple versions for the same package" do
1308+
let(:attributes) do
1309+
super().merge(
1310+
blocked_versions: [
1311+
{ "dependency-name" => "event-stream", "version" => "3.3.6", "reason" => "malware" },
1312+
{ "dependency-name" => "event-stream", "version" => "4.0.0", "reason" => "malware" }
1313+
]
1314+
)
1315+
end
1316+
1317+
it "includes all blocked versions" do
1318+
expect(ignored).to include("= 3.3.6", "= 4.0.0")
1319+
end
1320+
end
1321+
1322+
context "when blocked_versions does not match the dependency name" do
1323+
let(:attributes) do
1324+
super().merge(
1325+
blocked_versions: [
1326+
{ "dependency-name" => "other-package", "version" => "1.0.0", "reason" => "malware" }
1327+
]
1328+
)
1329+
end
1330+
1331+
it "does not include the blocked version" do
1332+
expect(ignored).not_to include("= 1.0.0")
1333+
end
1334+
end
1335+
1336+
context "when blocked_versions uses exact name matching (not wildcard)" do
1337+
let(:attributes) do
1338+
super().merge(
1339+
blocked_versions: [
1340+
{ "dependency-name" => "event-stream*", "version" => "3.3.6", "reason" => "malware" }
1341+
]
1342+
)
1343+
end
1344+
1345+
it "does not match wildcards in blocked version names" do
1346+
expect(ignored).not_to include("= 3.3.6")
1347+
end
1348+
end
1349+
1350+
context "when security_updates_only is true" do
1351+
let(:security_updates_only) { true }
1352+
let(:dependencies) { ["event-stream"] }
1353+
1354+
let(:attributes) do
1355+
super().merge(
1356+
blocked_versions: [
1357+
{ "dependency-name" => "event-stream", "version" => "3.3.6", "reason" => "malware" }
1358+
]
1359+
)
1360+
end
1361+
1362+
it "still includes the blocked version (blocks apply regardless of update type)" do
1363+
expect(ignored).to include("= 3.3.6")
1364+
end
1365+
end
1366+
1367+
context "when blocked_versions is empty" do
1368+
let(:attributes) do
1369+
super().merge(blocked_versions: [])
1370+
end
1371+
1372+
it "returns no additional conditions" do
1373+
expect(ignored).to be_empty
1374+
end
1375+
end
1376+
1377+
context "when blocked_versions contains malformed entries" do
1378+
let(:attributes) do
1379+
super().merge(
1380+
blocked_versions: [
1381+
{ "dependency-name" => "event-stream" },
1382+
{ "version" => "3.3.6" },
1383+
{},
1384+
{ "dependency-name" => "event-stream", "version" => "3.3.6", "reason" => "malware" }
1385+
]
1386+
)
1387+
end
1388+
1389+
it "ignores entries missing dependency-name or version" do
1390+
expect(ignored).to eq(["= 3.3.6"])
1391+
end
1392+
end
1393+
end
1394+
1395+
describe "#log_ignore_conditions_for with blocked_versions" do
1396+
let(:dependency) do
1397+
Dependabot::Dependency.new(
1398+
name: "event-stream",
1399+
package_manager: "bundler",
1400+
version: "3.3.5",
1401+
requirements: [{ file: "Gemfile", requirement: "~> 3.3", groups: [], source: nil }]
1402+
)
1403+
end
1404+
1405+
context "when blocked_versions contains a matching dependency" do
1406+
let(:attributes) do
1407+
super().merge(
1408+
blocked_versions: [
1409+
{
1410+
"dependency-name" => "event-stream",
1411+
"version" => "3.3.6",
1412+
"reason" => "malware - flatmap-stream injection"
1413+
}
1414+
]
1415+
)
1416+
end
1417+
1418+
it "logs the blocked version with reason" do
1419+
expect(Dependabot.logger).to receive(:info).with("Blocked versions (by GitHub Security):")
1420+
expect(Dependabot.logger).to receive(:info)
1421+
.with(" = 3.3.6 - reason: malware - flatmap-stream injection")
1422+
job.log_ignore_conditions_for(dependency)
1423+
end
1424+
end
1425+
1426+
context "when blocked_versions has no reason" do
1427+
let(:attributes) do
1428+
super().merge(
1429+
blocked_versions: [
1430+
{ "dependency-name" => "event-stream", "version" => "3.3.6" }
1431+
]
1432+
)
1433+
end
1434+
1435+
it "logs the blocked version without reason" do
1436+
expect(Dependabot.logger).to receive(:info).with("Blocked versions (by GitHub Security):")
1437+
expect(Dependabot.logger).to receive(:info).with(" = 3.3.6")
1438+
job.log_ignore_conditions_for(dependency)
1439+
end
1440+
end
1441+
1442+
context "when no blocked versions match" do
1443+
let(:attributes) do
1444+
super().merge(
1445+
blocked_versions: [
1446+
{ "dependency-name" => "other-pkg", "version" => "1.0.0", "reason" => "malware" }
1447+
]
1448+
)
1449+
end
1450+
1451+
it "does not log blocked versions" do
1452+
expect(Dependabot.logger).not_to receive(:info).with("Blocked versions (by GitHub Security):")
1453+
job.log_ignore_conditions_for(dependency)
1454+
end
1455+
end
1456+
end
12761457
end

0 commit comments

Comments
 (0)