Skip to content

Commit

Permalink
Merge pull request #7075 from dependabot/nishnha/plumb-dependency-groups
Browse files Browse the repository at this point in the history
Run Dependency Group updates
  • Loading branch information
Nishnha committed Apr 18, 2023
2 parents 797c9fd + 71ab200 commit e0c290b
Show file tree
Hide file tree
Showing 15 changed files with 579 additions and 43 deletions.
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
@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

0 comments on commit e0c290b

Please sign in to comment.