Skip to content

Commit 6145745

Browse files
authored
Fix allow update-types filtering for individual dependency updates (#14598)
1 parent 5f7cd4e commit 6145745

File tree

8 files changed

+486
-174
lines changed

8 files changed

+486
-174
lines changed

updater/lib/dependabot/job.rb

Lines changed: 106 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
module Dependabot
2929
class Job # rubocop:disable Metrics/ClassLength
3030
extend T::Sig
31-
include Dependabot::Updater::UpdateTypeHelper
3231

3332
TOP_LEVEL_DEPENDENCY_TYPES = T.let(%w(direct production development).freeze, T::Array[String])
3433
PERMITTED_KEYS = T.let(
@@ -301,7 +300,10 @@ def reject_external_code?
301300
# was vulnerable instead of the current version. This prevents security updates
302301
# from being filtered out after the dependency has already been updated in group scenarios.
303302
#
304-
# rubocop:disable Metrics/AbcSize
303+
# NOTE: update-types (semver-based filtering) is handled in ignore_conditions_for,
304+
# not here — allowed_update? runs pre-resolution when only the current version is
305+
# known, and semver-level filtering needs version ranges computed from that version.
306+
#
305307
# rubocop:disable Metrics/PerceivedComplexity
306308
# rubocop:disable Metrics/CyclomaticComplexity
307309
sig { params(dependency: Dependency, check_previous_version: T::Boolean).returns(T::Boolean) }
@@ -324,15 +326,6 @@ def allowed_update?(dependency, check_previous_version: false)
324326
condition_name = update.fetch("dependency-name", dependency.name)
325327
next false unless name_match?(condition_name, dependency.name)
326328

327-
# Check update-types (semver-based filtering) - security updates bypass this
328-
allowed_update_types = update.fetch("update-types", nil)
329-
if allowed_update_types.is_a?(Array) && !allowed_update_types.empty? && !security_update
330-
dep_update_type = update_type_for_dependency(dependency)
331-
config_type = "version-update:semver-#{dep_update_type}" if dep_update_type
332-
normalized_types = allowed_update_types.filter_map { |t| t.is_a?(String) ? t.downcase.strip : nil }
333-
next false if config_type && !normalized_types.include?(config_type)
334-
end
335-
336329
# Check the dependency-type (defaulting to all)
337330
dep_type = update.fetch("dependency-type", "all")
338331
next false if dep_type == "indirect" &&
@@ -348,7 +341,6 @@ def allowed_update?(dependency, check_previous_version: false)
348341
true
349342
end
350343
end
351-
# rubocop:enable Metrics/AbcSize
352344
# rubocop:enable Metrics/PerceivedComplexity
353345
# rubocop:enable Metrics/CyclomaticComplexity
354346

@@ -443,10 +435,16 @@ def security_advisories_for(dependency)
443435

444436
sig { params(dependency: Dependabot::Dependency).returns(T::Array[String]) }
445437
def ignore_conditions_for(dependency)
446-
update_config.ignored_versions_for(
438+
conditions = update_config.ignored_versions_for(
447439
dependency,
448440
security_updates_only: security_updates_only?
449441
)
442+
443+
# Supplement with implicit ignore ranges derived from allow update-types.
444+
# allow update-types cannot be checked in allowed_update? because it runs
445+
# pre-resolution when only the current version is known. Version ranges
446+
# only need the current version — the same mechanism as ignore update-types.
447+
conditions + ignored_versions_from_allowed_update_types(dependency)
450448
end
451449

452450
# TODO: Present Dependabot::Config::IgnoreCondition in calling code
@@ -485,6 +483,101 @@ def completely_ignored?(dependency)
485483
ignore_conditions_for(dependency).any?(Dependabot::Config::IgnoreCondition::ALL_VERSIONS)
486484
end
487485

486+
# Derives implicit ignore version ranges from allow rules that specify update-types.
487+
# For example, if allow says only "semver-patch", this computes ignore ranges for
488+
# major and minor — using the same mechanism as IgnoreCondition#versions_by_type.
489+
sig { params(dependency: Dependabot::Dependency).returns(T::Array[String]) }
490+
def ignored_versions_from_allowed_update_types(dependency)
491+
return [] if security_updates_only?
492+
493+
permitted_types = collect_permitted_update_types(dependency)
494+
return [] if permitted_types.empty?
495+
496+
disallowed_types = Dependabot::Updater::UpdateTypeHelper::ALL_SEMVER_UPDATE_TYPES - permitted_types
497+
498+
version = version_for_dependency(dependency)
499+
return [] unless version
500+
501+
disallowed_types.flat_map do |t|
502+
case t
503+
when Dependabot::Config::IgnoreCondition::PATCH_VERSION_TYPE
504+
version.ignored_patch_versions
505+
when Dependabot::Config::IgnoreCondition::MINOR_VERSION_TYPE
506+
version.ignored_minor_versions
507+
when Dependabot::Config::IgnoreCondition::MAJOR_VERSION_TYPE
508+
version.ignored_major_versions
509+
else
510+
[]
511+
end
512+
end.compact
513+
end
514+
515+
# Collects the union of update-types from all matching allow rules for a dependency.
516+
# Returns empty if no matching rules specify update-types (meaning no filtering needed).
517+
# If any matching rule lacks update-types, it permits all types — returns empty.
518+
sig { params(dependency: Dependabot::Dependency).returns(T::Array[String]) }
519+
def collect_permitted_update_types(dependency)
520+
matching_rules = matching_allow_rules(dependency)
521+
return [] if matching_rules.empty?
522+
523+
# If any matching rule lacks update-types, it permits all types
524+
return [] if matching_rules.any? { |r| allow_rule_permits_all_types?(r) }
525+
526+
matching_rules
527+
.flat_map { |r| r.fetch("update-types", []) }
528+
.filter_map { |t| t.is_a?(String) ? t.downcase.strip : nil }
529+
.select { |t| Dependabot::Updater::UpdateTypeHelper::ALL_SEMVER_UPDATE_TYPES.include?(t) }
530+
.uniq
531+
end
532+
533+
sig { params(dependency: Dependabot::Dependency).returns(T::Array[T::Hash[String, T.untyped]]) }
534+
def matching_allow_rules(dependency)
535+
allowed_updates.select do |update|
536+
allow_rule_matches_dependency?(update, dependency)
537+
end
538+
end
539+
540+
sig { params(update: T::Hash[String, T.untyped], dependency: Dependabot::Dependency).returns(T::Boolean) }
541+
def allow_rule_matches_dependency?(update, dependency)
542+
condition_name = update.fetch("dependency-name", nil)
543+
return false if condition_name && !name_match?(condition_name, dependency.name)
544+
545+
dep_type = update.fetch("dependency-type", nil)
546+
return true if dep_type.nil? || dep_type == "all"
547+
548+
# Indirect deps don't match top-level type rules (matching allowed_update? behavior)
549+
return false if dependency.requirements.none? && TOP_LEVEL_DEPENDENCY_TYPES.include?(dep_type)
550+
551+
case dep_type
552+
when "production" then dependency.production?
553+
when "development" then !dependency.production?
554+
when "direct" then dependency.requirements.any?
555+
when "indirect" then dependency.requirements.none?
556+
else true
557+
end
558+
end
559+
560+
sig { params(rule: T::Hash[String, T.untyped]).returns(T::Boolean) }
561+
def allow_rule_permits_all_types?(rule)
562+
!rule.key?("update-types") || !rule["update-types"].is_a?(Array) || rule["update-types"].empty?
563+
end
564+
565+
sig { params(dependency: Dependabot::Dependency).returns(T.nilable(Dependabot::Version)) }
566+
def version_for_dependency(dependency)
567+
version_str = dependency.version
568+
return nil if version_str.nil? || version_str.empty?
569+
570+
version_class = begin
571+
Dependabot::Utils.version_class_for_package_manager(dependency.package_manager)
572+
rescue StandardError
573+
Dependabot::Version
574+
end
575+
576+
return nil unless version_class.correct?(version_str)
577+
578+
version_class.new(version_str)
579+
end
580+
488581
sig { void }
489582
def register_experiments
490583
experiments.entries.each do |name, value|

updater/lib/dependabot/updater/update_type_helper.rb

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# frozen_string_literal: true
33

44
require "sorbet-runtime"
5+
require "dependabot/config/ignore_condition"
56
require "dependabot/utils"
67

78
module Dependabot
@@ -12,6 +13,19 @@ class Updater
1213
module UpdateTypeHelper
1314
extend T::Sig
1415

16+
SEMVER_MAJOR = "major"
17+
SEMVER_MINOR = "minor"
18+
SEMVER_PATCH = "patch"
19+
20+
ALL_SEMVER_UPDATE_TYPES = T.let(
21+
[
22+
Dependabot::Config::IgnoreCondition::MAJOR_VERSION_TYPE,
23+
Dependabot::Config::IgnoreCondition::MINOR_VERSION_TYPE,
24+
Dependabot::Config::IgnoreCondition::PATCH_VERSION_TYPE
25+
].freeze,
26+
T::Array[String]
27+
)
28+
1529
# Represents semantic version components (major, minor, patch)
1630
class SemverParts < T::Struct
1731
const :major, Integer
@@ -58,11 +72,11 @@ def classify_semver_update(prev, curr)
5872
curr_parts = semver_parts(curr)
5973
return nil if prev_parts.nil? || curr_parts.nil?
6074

61-
return "major" if curr_parts.major > prev_parts.major
62-
return "minor" if curr_parts.major == prev_parts.major && curr_parts.minor > prev_parts.minor
63-
return "patch" if curr_parts.major == prev_parts.major &&
64-
curr_parts.minor == prev_parts.minor &&
65-
curr_parts.patch > prev_parts.patch
75+
return SEMVER_MAJOR if curr_parts.major > prev_parts.major
76+
return SEMVER_MINOR if curr_parts.major == prev_parts.major && curr_parts.minor > prev_parts.minor
77+
return SEMVER_PATCH if curr_parts.major == prev_parts.major &&
78+
curr_parts.minor == prev_parts.minor &&
79+
curr_parts.patch > prev_parts.patch
6680

6781
Dependabot.logger.info(
6882
"Could not classify semver update: #{prev_parts.major}.#{prev_parts.minor}.#{prev_parts.patch} -> " \

0 commit comments

Comments
 (0)