@@ -35,7 +35,12 @@ def self.from_job_config(job:)
3535 validate_group_configuration! ( job )
3636
3737 groups = job . dependency_groups . map do |group |
38- Dependabot ::DependencyGroup . new ( name : group [ "name" ] , rules : group [ "rules" ] , applies_to : group [ "applies-to" ] )
38+ Dependabot ::DependencyGroup . new (
39+ name : group [ "name" ] ,
40+ rules : group [ "rules" ] ,
41+ applies_to : group [ "applies-to" ] ,
42+ group_by : group . dig ( "rules" , "group-by" )
43+ )
3944 end
4045
4146 # Filter out version updates when doing security updates and visa versa
@@ -92,20 +97,8 @@ def find_group(name:)
9297 sig { params ( dependencies : T ::Array [ Dependabot ::Dependency ] ) . void }
9398 def assign_to_groups! ( dependencies :)
9499 if dependency_groups . any?
95- specificity_calculator = Dependabot ::Updater ::PatternSpecificityCalculator . new
96-
97- dependencies . each do |dependency |
98- matched_groups = @dependency_groups . each_with_object ( [ ] ) do |group , matches |
99- next unless group . contains? ( dependency )
100- next if should_skip_due_to_specificity? ( group , dependency , specificity_calculator )
101-
102- group . dependencies . push ( dependency )
103- matches << group
104- end
105-
106- # If we had no matches, collect the dependency as ungrouped
107- @ungrouped_dependencies << dependency if matched_groups . empty?
108- end
100+ assign_dependencies_to_groups ( dependencies )
101+ create_dynamic_subgroups_for_dependency_name_groups ( dependencies )
109102 else
110103 @ungrouped_dependencies += dependencies
111104 end
@@ -115,6 +108,48 @@ def assign_to_groups!(dependencies:)
115108
116109 private
117110
111+ sig { params ( dependencies : T ::Array [ Dependabot ::Dependency ] ) . void }
112+ def assign_dependencies_to_groups ( dependencies )
113+ specificity_calculator = Dependabot ::Updater ::PatternSpecificityCalculator . new
114+
115+ dependencies . each do |dependency |
116+ matched_groups = assign_dependency_to_matching_groups ( dependency , specificity_calculator )
117+ mark_ungrouped_if_no_matches ( dependency , matched_groups )
118+ end
119+ end
120+
121+ sig do
122+ params (
123+ dependency : Dependabot ::Dependency ,
124+ specificity_calculator : Dependabot ::Updater ::PatternSpecificityCalculator
125+ ) . returns ( T ::Array [ Dependabot ::DependencyGroup ] )
126+ end
127+ def assign_dependency_to_matching_groups ( dependency , specificity_calculator )
128+ @dependency_groups . each_with_object ( [ ] ) do |group , matches |
129+ next if group . group_by_dependency_name?
130+ next unless group . contains? ( dependency )
131+ next if should_skip_due_to_specificity? ( group , dependency , specificity_calculator )
132+
133+ group . dependencies . push ( dependency )
134+ matches << group
135+ end
136+ end
137+
138+ sig { params ( dependency : Dependabot ::Dependency , matched_groups : T ::Array [ Dependabot ::DependencyGroup ] ) . void }
139+ def mark_ungrouped_if_no_matches ( dependency , matched_groups )
140+ return unless matched_groups . empty?
141+ return if matches_group_by_parent_group? ( dependency )
142+
143+ @ungrouped_dependencies << dependency
144+ end
145+
146+ sig { params ( dependency : Dependabot ::Dependency ) . returns ( T ::Boolean ) }
147+ def matches_group_by_parent_group? ( dependency )
148+ @dependency_groups . any? do |group |
149+ group . group_by_dependency_name? && group . contains? ( dependency )
150+ end
151+ end
152+
118153 sig { params ( dependency_groups : T ::Array [ Dependabot ::DependencyGroup ] ) . void }
119154 def initialize ( dependency_groups :)
120155 @dependency_groups = dependency_groups
@@ -123,7 +158,11 @@ def initialize(dependency_groups:)
123158
124159 sig { void }
125160 def validate_groups
126- empty_groups = dependency_groups . select { |group | group . dependencies . empty? }
161+ # Exclude parent groups with group_by_dependency_name? from empty group warnings
162+ # as they intentionally have no direct dependencies (subgroups have them)
163+ empty_groups = dependency_groups . select do |group |
164+ group . dependencies . empty? && !group . group_by_dependency_name?
165+ end
127166 warn_misconfigured_groups ( empty_groups ) if empty_groups . any?
128167 end
129168
@@ -171,5 +210,27 @@ def should_skip_due_to_specificity?(group, dependency, specificity_calculator)
171210
172211 false
173212 end
213+
214+ sig { params ( dependencies : T ::Array [ Dependabot ::Dependency ] ) . void }
215+ def create_dynamic_subgroups_for_dependency_name_groups ( dependencies )
216+ parent_groups = @dependency_groups . select ( &:group_by_dependency_name? )
217+
218+ parent_groups . each do |parent_group |
219+ matching_deps = dependencies . select { |dep | parent_group . contains? ( dep ) }
220+
221+ matching_deps . group_by ( &:name ) . each do |dep_name , deps |
222+ subgroup = Dependabot ::DependencyGroup . new (
223+ name : "#{ parent_group . name } /#{ dep_name } " ,
224+ rules : parent_group . rules . merge ( "patterns" => [ dep_name ] ) ,
225+ applies_to : parent_group . applies_to
226+ # NOTE: subgroups don't inherit group_by to prevent infinite recursion
227+ )
228+ subgroup . dependencies . concat ( deps )
229+ @dependency_groups << subgroup
230+ end
231+
232+ parent_group . dependencies . clear
233+ end
234+ end
174235 end
175236end
0 commit comments