Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ff792bc
GroupRule -> DependencyGroup
Nishnha Apr 5, 2023
e8a2b4b
Fix typo on DependencyChange
Nishnha Apr 12, 2023
ccafaa6
Add dependency_groups to a Job
Nishnha Apr 6, 2023
740a469
Add a dependency groups job fixture
Nishnha Apr 13, 2023
e9f8b38
Add #rules and #contains? to DependencyGroups
Nishnha Apr 12, 2023
f35246a
Add DependencyGroup tests
Nishnha Apr 13, 2023
45dd99b
Introduce the DependencyGroupEngine
Nishnha Apr 12, 2023
60b3b24
Wire the DependencyGroupEngine into GroupUpdateAllVersions
Nishnha Apr 12, 2023
f098677
Wire the DependencyGroupEngine to a Job
Nishnha Apr 12, 2023
d4023a8
Test that a Job registers its dependency groups
Nishnha Apr 13, 2023
ed11117
Add dependency group gem fixtures
Nishnha Apr 13, 2023
908c52e
Add DependencyGroupEngine tests
Nishnha Apr 13, 2023
64b9f95
Update one DependencySnapshot group at a time in GroupUpdateAllVersions
Nishnha Apr 12, 2023
62319d6
Merge branch 'main' into nishnha/plumb-dependency-groups
Nishnha Apr 13, 2023
a8aa686
Clean up the wording of tests and comments
Nishnha Apr 13, 2023
b95cf42
Remove the unused #belongs_to_dependency_group? method
Nishnha Apr 13, 2023
bff02ec
Use #each instead of #inject
Nishnha Apr 13, 2023
83f0b59
Move WildcardMatcher to common
Nishnha Apr 13, 2023
0b9675f
Use the WildcardMatcher in common
Nishnha Apr 13, 2023
9ab1df5
Fix how DependencyGroups are registered
Nishnha Apr 13, 2023
109b1c5
Update Job fixture to use the correct dependency groups format
Nishnha Apr 13, 2023
99a5c63
Use named parameters for DependencyGroup
Nishnha Apr 17, 2023
4bc4163
Preserve default group-all behavior
Nishnha Apr 17, 2023
4e357b6
Merge branch 'main' into nishnha/plumb-dependency-groups
Nishnha Apr 17, 2023
8c55b80
Fix flakey test and smoke tests
Nishnha Apr 17, 2023
62d47bc
register all dependencies group unless any dependency groups exist
Nishnha Apr 17, 2023
0bcc0f7
Merge branch 'main' into nishnha/plumb-dependency-groups
Nishnha Apr 17, 2023
9ac2f7e
Don't register any dependency groups in the job spec
Nishnha Apr 18, 2023
71ab200
Merge branch 'main' into nishnha/plumb-dependency-groups
Nishnha Apr 18, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion common/lib/dependabot/dependency_group.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
# frozen_string_literal: true

require "wildcard_matcher"

module Dependabot
class DependencyGroup
attr_reader :name, :rules
attr_reader :name, :rules, :dependencies

def initialize(name:, rules:)
@name = name
@rules = rules
@dependencies = []
end

def contains?(dependency)
@dependencies.include?(dependency) if @dependencies.any?
rules.any? { |rule| WildcardMatcher.match?(rule, dependency.name) }
end
end
end
File renamed without changes.
97 changes: 94 additions & 3 deletions common/spec/dependabot/dependency_group_spec.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,105 @@
# frozen_string_literal: true

require "dependabot/dependency_group"
require "dependabot/dependency"

# TODO: Once the Updater has been merged into Core, we should test this
# using the DependencyGroupEngine methods instead of mocking the functionality
RSpec.describe Dependabot::DependencyGroup do
let(:dependency_group) { described_class.new(name: name, rules: rules) }
let(:name) { "test_group" }
let(:rules) { ["test-*"] }

let(:test_dependency1) do
Dependabot::Dependency.new(
name: "test-dependency-1",
package_manager: "bundler",
version: "1.1.0",
requirements: [
{
file: "Gemfile",
requirement: "~> 1.1.0",
groups: [],
source: nil
}
]
)
end

let(:test_dependency2) do
Dependabot::Dependency.new(
name: "another-test-dependency",
package_manager: "bundler",
version: "1.1.0",
requirements: [
{
file: "Gemfile",
requirement: "~> 1.1.0",
groups: [],
source: nil
}
]
)
end

describe "#name" do
it "returns the name" do
my_dependency_group_name = "darren-from-work"
dependency_group = described_class.new(name: my_dependency_group_name, rules: anything)
expect(dependency_group.name).to eq(name)
end
end

describe "#rules" do
it "returns a list of rules" do
expect(dependency_group.rules).to eq(rules)
end
end

describe "#dependencies" do
context "when no dependencies are assigned to the group" do
it "returns an empty list" do
expect(dependency_group.dependencies).to eq([])
end
end

context "when dependencies have been assigned" do
before do
dependency_group.dependencies << test_dependency1
end

it "returns the dependencies" do
expect(dependency_group.dependencies).to include(test_dependency1)
expect(dependency_group.dependencies).not_to include(test_dependency2)
end
end
end

describe "#contains?" do
context "before dependencies are assigned to the group" do
it "returns true if the dependency matches a rule" do
expect(dependency_group.dependencies).to eq([])
expect(dependency_group.contains?(test_dependency1)).to be_truthy
end

it "returns false if the dependency does not match a rule" do
expect(dependency_group.dependencies).to eq([])
expect(dependency_group.contains?(test_dependency2)).to be_falsey
end
end

context "after dependencies are assigned to the group" do
before do
dependency_group.dependencies << test_dependency1
end

it "returns true if the dependency is in the dependency list" do
expect(dependency_group.dependencies).to include(test_dependency1)
expect(dependency_group.contains?(test_dependency1)).to be_truthy
end

expect(dependency_group.name).to eq(my_dependency_group_name)
it "returns false if the dependency is not in the dependency list and does not match a rule" do
expect(dependency_group.dependencies).to include(test_dependency1)
expect(dependency_group.contains?(test_dependency2)).to be_falsey
end
end
end
end
80 changes: 80 additions & 0 deletions updater/lib/dependabot/dependency_group_engine.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# frozen_string_literal: true

require "dependabot/dependency_group"

# This class implements our strategy for keeping track of and matching dependency
# groups that are defined by users in their dependabot config file.
#
# This is a static class tied to the lifecycle of a Job
# - Each UpdateJob registers its own DependencyGroupEngine which calculates
# the grouped and ungrouped dependencies for a DependencySnapshot
# - Groups are only calculated once after the Job has registered its dependencies
# - All allowed dependencies should be passed in to the calculate_dependency_groups! method
#
# **Note:** This is currently an experimental feature which is not supported
# in the service or as an integration point.
#
module Dependabot
module DependencyGroupEngine
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
module DependencyGroupEngine
class DependencyGroupEngine

I think it's a little unusual to have a module with instance variables, it works but it isn't a common idiom.

I think as a starting point it is fine to just make this a class and then prevent it being instantiated via:

private

def initialize
end

That said, I do wonder if it might be easier to just simplify this class into something we instantiate in DependancySnapshot right after we parse the dependency files.

That would make the affordances and testing a little more idiomatic and it would have the benefit that if the group rule parsing blew up for any reason, it would happen at the point the snapshot is being set up, like parsing, rather than lazily once we start into the operation class.

That would keep any group-related error handling ( i.e. invalid group names, etc ) out of the way of the actual update operation which would keep things nice and clear.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That said, I think we can leave this as-is since it is working for this PR and refactor it in a follow-up, overall how this works is nice and clear 👍🏻

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks I'll take a look at this in a follow up PR
re: #7075 (comment)

@groups_calculated = false
@registered_groups = []

@dependency_groups = {}
@ungrouped_dependencies = []

def self.reset!
@groups_calculated = false
@registered_groups = []

@dependency_groups = {}
@ungrouped_dependencies = []
end

# Eventually the key for a dependency group should be a hash since names _can_ conflict within jobs
def self.register(name, rules)
@registered_groups.push Dependabot::DependencyGroup.new(name: name, rules: rules)
end

def self.groups_for(dependency)
return [] if dependency.nil?
return [] unless dependency.instance_of?(Dependabot::Dependency)

@registered_groups.select do |group|
group.contains?(dependency)
end
end

# { group_name => [DependencyGroup], ... }
def self.dependency_groups(dependencies)
return @dependency_groups if @groups_calculated

@groups_calculated = calculate_dependency_groups!(dependencies)

@dependency_groups
end

# Returns a list of dependencies that do not belong to any of the groups
def self.ungrouped_dependencies(dependencies)
return @ungrouped_dependencies if @groups_calculated

@groups_calculated = calculate_dependency_groups!(dependencies)

@ungrouped_dependencies
end

def self.calculate_dependency_groups!(dependencies)
dependencies.each do |dependency|
groups = groups_for(dependency)

@ungrouped_dependencies << dependency if groups.empty?

groups.each do |group|
group.dependencies.push(dependency)
@dependency_groups[group.name.to_sym] = group
end
end

true
end
end
end
12 changes: 12 additions & 0 deletions updater/lib/dependabot/dependency_snapshot.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,18 @@ def job_dependencies
end
end

# A dependency snapshot will always have the same set of dependencies since it only depends
# on the Job and dependency groups, which are static for a given commit.
def groups
# The DependencyGroupEngine registers dependencies when the Job is created
# and it will memoize the dependency groups
Dependabot::DependencyGroupEngine.dependency_groups(allowed_dependencies)
end

def ungrouped_dependencies
Dependabot::DependencyGroupEngine.ungrouped_dependencies(allowed_dependencies)
end

private

def initialize(job:, base_commit_sha:, dependency_files:)
Expand Down
15 changes: 14 additions & 1 deletion updater/lib/dependabot/job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require "dependabot/config/ignore_condition"
require "dependabot/config/update_config"
require "dependabot/dependency_group_engine"
require "dependabot/experiments"
require "dependabot/source"
require "wildcard_matcher"
Expand Down Expand Up @@ -37,6 +38,7 @@ class Job
update_subdependencies
updating_a_pull_request
vendor_dependencies
dependency_groups
)

attr_reader :allowed_updates,
Expand All @@ -51,7 +53,8 @@ class Job
:security_updates_only,
:source,
:token,
:vendor_dependencies
:vendor_dependencies,
:dependency_groups

def self.new_fetch_job(job_id:, job_definition:, repo_contents_path: nil)
attrs = standardise_keys(job_definition["job"]).slice(*PERMITTED_KEYS)
Expand Down Expand Up @@ -94,8 +97,10 @@ def initialize(attributes)
@update_subdependencies = attributes.fetch(:update_subdependencies)
@updating_a_pull_request = attributes.fetch(:updating_a_pull_request)
@vendor_dependencies = attributes.fetch(:vendor_dependencies, false)
@dependency_groups = attributes.fetch(:dependency_groups, [])

register_experiments
register_dependency_groups
end

def clone?
Expand Down Expand Up @@ -233,6 +238,14 @@ def security_advisories_for(dependency)
end
end

def register_dependency_groups
return if dependency_groups.nil?

dependency_groups.each do |group|
Dependabot::DependencyGroupEngine.register(group["name"], group["rules"]["patterns"])
end
end

def ignore_conditions_for(dependency)
update_config.ignored_versions_for(
dependency,
Expand Down
Loading