Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 13 additions & 0 deletions updater/lib/dependabot/job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
require "dependabot/source"
require "dependabot/pull_request"
require "dependabot/package/release_cooldown_options"
require "dependabot/updater/update_type_helper"

# Describes a single Dependabot workload within the GitHub-integrated Service
#
Expand All @@ -27,6 +28,7 @@
module Dependabot
class Job # rubocop:disable Metrics/ClassLength
extend T::Sig
include Dependabot::Updater::UpdateTypeHelper

TOP_LEVEL_DEPENDENCY_TYPES = T.let(%w(direct production development).freeze, T::Array[String])
PERMITTED_KEYS = T.let(
Expand Down Expand Up @@ -299,6 +301,7 @@ def reject_external_code?
# was vulnerable instead of the current version. This prevents security updates
# from being filtered out after the dependency has already been updated in group scenarios.
#
# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/PerceivedComplexity
# rubocop:disable Metrics/CyclomaticComplexity
sig { params(dependency: Dependency, check_previous_version: T::Boolean).returns(T::Boolean) }
Expand All @@ -321,6 +324,15 @@ def allowed_update?(dependency, check_previous_version: false)
condition_name = update.fetch("dependency-name", dependency.name)
next false unless name_match?(condition_name, dependency.name)

# Check update-types (semver-based filtering) - security updates bypass this
allowed_update_types = update.fetch("update-types", nil)
if allowed_update_types.is_a?(Array) && !allowed_update_types.empty? && !security_update
dep_update_type = update_type_for_dependency(dependency)
config_type = "version-update:semver-#{dep_update_type}" if dep_update_type
normalized_types = allowed_update_types.filter_map { |t| t.is_a?(String) ? t.downcase.strip : nil }
next false if config_type && !normalized_types.include?(config_type)
end

# Check the dependency-type (defaulting to all)
dep_type = update.fetch("dependency-type", "all")
next false if dep_type == "indirect" &&
Expand All @@ -336,6 +348,7 @@ def allowed_update?(dependency, check_previous_version: false)
true
end
end
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/PerceivedComplexity
# rubocop:enable Metrics/CyclomaticComplexity

Expand Down
19 changes: 0 additions & 19 deletions updater/lib/dependabot/updater/group_dependency_selector.rb
Original file line number Diff line number Diff line change
Expand Up @@ -281,25 +281,6 @@ def group_applies_to
T.unsafe(@group).applies_to
end

sig { params(dep: Dependabot::Dependency).returns(T.nilable(String)) }
def update_type_for_dependency(dep)
prev_str = dep.respond_to?(:previous_version) ? dep.previous_version&.to_s : nil
curr_str = dep.respond_to?(:version) ? dep.version&.to_s : nil
return nil unless prev_str && curr_str

version_class = version_class_for(dep)
return nil unless version_class

update_type = update_type_from_class(version_class, prev_str, curr_str)
return update_type if update_type

versions = build_versions(version_class, prev_str, curr_str)
return nil unless versions

prev_ver, curr_ver = versions
classify_semver_update(prev_ver, curr_ver)
end

sig { params(dep: Dependabot::Dependency, job: Dependabot::Job).returns(T::Boolean) }
def allowed_by_config?(dep, job)
ignore_conditions = job.ignore_conditions_for(dep)
Expand Down
21 changes: 21 additions & 0 deletions updater/lib/dependabot/updater/update_type_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,27 @@ def semver_parts(version)

SemverParts.new(major: major, minor: minor, patch: patch)
end

# Determines the semver update type ("major", "minor", "patch") for a dependency
# by comparing its previous and current versions. Returns nil if it cannot be determined.
sig { params(dep: Dependabot::Dependency).returns(T.nilable(String)) }
def update_type_for_dependency(dep)
prev_str = dep.respond_to?(:previous_version) ? dep.previous_version&.to_s : nil
curr_str = dep.respond_to?(:version) ? dep.version&.to_s : nil
return nil unless prev_str && curr_str

version_class = version_class_for(dep)
return nil unless version_class

update_type = update_type_from_class(version_class, prev_str, curr_str)
return update_type if update_type

versions = build_versions(version_class, prev_str, curr_str)
return nil unless versions

prev_ver, curr_ver = versions
classify_semver_update(prev_ver, curr_ver)
end
end
end
end
233 changes: 233 additions & 0 deletions updater/spec/dependabot/job_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,239 @@

it { is_expected.to be(false) }
end

context "with update-types in allow block" do
let(:dependency) do
Dependabot::Dependency.new(
name: "business",
package_manager: "bundler",
version: "1.9.0",
previous_version: "1.8.0",
requirements: requirements
)
end

context "when allowing minor updates and dependency has minor update" do
let(:allowed_updates) do
[
{
"dependency-name" => "business",
"update-types" => ["version-update:semver-minor"]
}
]
end

it { is_expected.to be(true) }
end

context "when allowing only patch updates but dependency has minor update" do
let(:allowed_updates) do
[
{
"dependency-name" => "business",
"update-types" => ["version-update:semver-patch"]
}
]
end

it { is_expected.to be(false) }
end

context "when allowing multiple update types including minor" do
let(:allowed_updates) do
[
{
"dependency-name" => "business",
"update-types" => ["version-update:semver-patch", "version-update:semver-minor"]
}
]
end

it { is_expected.to be(true) }
end

context "with a major version update" do
let(:dependency) do
Dependabot::Dependency.new(
name: "business",
package_manager: "bundler",
version: "2.0.0",
previous_version: "1.8.0",
requirements: requirements
)
end

context "when allowing major updates" do
let(:allowed_updates) do
[
{
"dependency-name" => "business",
"update-types" => ["version-update:semver-major"]
}
]
end

it { is_expected.to be(true) }
end

context "when only allowing minor updates" do
let(:allowed_updates) do
[
{
"dependency-name" => "business",
"update-types" => ["version-update:semver-minor"]
}
]
end

it { is_expected.to be(false) }
end
end

context "with a patch version update" do
let(:dependency) do
Dependabot::Dependency.new(
name: "business",
package_manager: "bundler",
version: "1.8.1",
previous_version: "1.8.0",
requirements: requirements
)
end

let(:allowed_updates) do
[
{
"dependency-name" => "business",
"update-types" => ["version-update:semver-patch"]
}
]
end

it { is_expected.to be(true) }
end

context "when combining update-types with dependency-type" do
let(:allowed_updates) do
[
{
"dependency-name" => "business",
"dependency-type" => "direct",
"update-types" => ["version-update:semver-minor", "version-update:semver-patch"]
}
]
end

it { is_expected.to be(true) }
end

context "when dependency has no previous version" do
let(:dependency) do
Dependabot::Dependency.new(
name: "business",
package_manager: "bundler",
version: "1.9.0",
previous_version: nil,
requirements: requirements
)
end

let(:allowed_updates) do
[
{
"dependency-name" => "business",
"update-types" => ["version-update:semver-minor"]
}
]
end

it "allows update when semver type cannot be determined" do
expect(allowed_update).to be(true)
end
end

context "when dependency name does not match" do
let(:allowed_updates) do
[
{
"dependency-name" => "other-dep",
"update-types" => ["version-update:semver-minor"]
}
]
end

it { is_expected.to be(false) }
end

context "when update-types is empty array" do
let(:allowed_updates) do
[
{
"dependency-name" => "business",
"update-types" => []
}
]
end

it "treats empty update-types as no filtering" do
expect(allowed_update).to be(true)
end
end

context "when one allow rule has update-types and another does not" do
let(:dependency) do
Dependabot::Dependency.new(
name: "business",
package_manager: "bundler",
version: "2.0.0",
previous_version: "1.0.0",
requirements: requirements
)
end

let(:allowed_updates) do
[
{
"dependency-name" => "business",
"update-types" => ["version-update:semver-patch"]
},
{
"dependency-name" => "business"
}
]
end

it "allows if any rule matches (second rule has no update-types filter)" do
expect(allowed_update).to be(true)
end
end

context "with security updates" do
let(:security_advisories) do
[
{
"dependency-name" => "business",
"affected-versions" => [],
"patched-versions" => ["~> 1.11.0"],
"unaffected-versions" => []
}
]
end

let(:allowed_updates) do
[
{
"update-type" => "security",
"update-types" => ["version-update:semver-patch"]
}
]
end

it "bypasses update-types filtering for security updates" do
expect(allowed_update).to be(true)
end
end
end
end

describe "#security_updates_only?" do
Expand Down
Loading
Loading