Skip to content

Commit b210204

Browse files
authored
Merge pull request #14008 from dependabot/feat/group-by-dependency-name-engine
feat: Add dynamic subgroup creation in DependencyGroupEngine
2 parents e7b8811 + b1cb1fd commit b210204

File tree

3 files changed

+339
-16
lines changed

3 files changed

+339
-16
lines changed

updater/lib/dependabot/dependency_group_engine.rb

Lines changed: 77 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -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
175236
end

0 commit comments

Comments
 (0)