Skip to content

Commit 742e435

Browse files
committed
feat: add semantic-versioning option
1 parent e37c650 commit 742e435

File tree

9 files changed

+549
-21
lines changed

9 files changed

+549
-21
lines changed

common/lib/dependabot/config/ignore_condition.rb

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,18 @@ def initialize(dependency_name:, versions: nil, update_types: nil)
3737
@update_types = T.let(update_types || [], T::Array[String])
3838
end
3939

40-
sig { params(dependency: Dependency, security_updates_only: T::Boolean).returns(T::Array[String]) }
41-
def ignored_versions(dependency, security_updates_only)
40+
sig do
41+
params(
42+
dependency: Dependency,
43+
security_updates_only: T::Boolean,
44+
semantic_versioning: String
45+
).returns(T::Array[String])
46+
end
47+
def ignored_versions(dependency, security_updates_only, semantic_versioning: "relaxed")
4248
return versions if security_updates_only
4349
return [ALL_VERSIONS] if versions.empty? && transformed_update_types.empty?
4450

45-
versions_by_type(dependency) + versions
51+
versions_by_type(dependency, semantic_versioning: semantic_versioning) + versions
4652
end
4753

4854
private
@@ -52,19 +58,19 @@ def transformed_update_types
5258
update_types.map(&:downcase).filter_map(&:strip)
5359
end
5460

55-
sig { params(dependency: Dependency).returns(T::Array[T.untyped]) }
56-
def versions_by_type(dependency)
61+
sig { params(dependency: Dependency, semantic_versioning: String).returns(T::Array[T.untyped]) }
62+
def versions_by_type(dependency, semantic_versioning: "relaxed")
5763
version = correct_version_for(dependency)
5864
return [] unless version
5965

6066
transformed_update_types.flat_map do |t|
6167
case t
6268
when PATCH_VERSION_TYPE
63-
version.ignored_patch_versions
69+
version.ignored_patch_versions_for_mode(semantic_versioning)
6470
when MINOR_VERSION_TYPE
65-
version.ignored_minor_versions
71+
version.ignored_minor_versions_for_mode(semantic_versioning)
6672
when MAJOR_VERSION_TYPE
67-
version.ignored_major_versions
73+
version.ignored_major_versions_for_mode(semantic_versioning)
6874
else
6975
[]
7076
end

common/lib/dependabot/config/update_config.rb

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,14 @@ def initialize(ignore_conditions: nil, commit_message_options: nil, exclude_path
3232
@exclude_paths = exclude_paths
3333
end
3434

35-
sig { params(dependency: Dependency, security_updates_only: T::Boolean).returns(T::Array[String]) }
36-
def ignored_versions_for(dependency, security_updates_only: false)
35+
sig do
36+
params(
37+
dependency: Dependency,
38+
security_updates_only: T::Boolean,
39+
semantic_versioning: String
40+
).returns(T::Array[String])
41+
end
42+
def ignored_versions_for(dependency, security_updates_only: false, semantic_versioning: "relaxed")
3743
normalizer = name_normaliser_for(dependency)
3844
dep_name = T.must(normalizer).call(dependency.name)
3945

@@ -43,7 +49,7 @@ def ignored_versions_for(dependency, security_updates_only: false)
4349

4450
@ignore_conditions
4551
.select { |ic| self.class.wildcard_match?(T.must(normalizer).call(ic.dependency_name), dep_name) }
46-
.map { |ic| ic.ignored_versions(dependency, security_updates_only) }
52+
.map { |ic| ic.ignored_versions(dependency, security_updates_only, semantic_versioning: semantic_versioning) }
4753
.flatten
4854
.compact
4955
.uniq

common/lib/dependabot/version.rb

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,32 @@ def lowest_prerelease_suffix
7474
"a"
7575
end
7676

77+
# Mode-aware methods that respect semantic-versioning option
78+
# When semantic_versioning is "strict", 0.x versions are treated differently:
79+
# - 0.0.z: any change is breaking (major)
80+
# - 0.y.z: minor changes are breaking (major)
81+
82+
sig { params(semantic_versioning: String).returns(T::Array[String]) }
83+
def ignored_patch_versions_for_mode(semantic_versioning = "relaxed")
84+
return strict_ignored_patch_versions if semantic_versioning == "strict"
85+
86+
ignored_patch_versions
87+
end
88+
89+
sig { params(semantic_versioning: String).returns(T::Array[String]) }
90+
def ignored_minor_versions_for_mode(semantic_versioning = "relaxed")
91+
return strict_ignored_minor_versions if semantic_versioning == "strict"
92+
93+
ignored_minor_versions
94+
end
95+
96+
sig { params(semantic_versioning: String).returns(T::Array[String]) }
97+
def ignored_major_versions_for_mode(semantic_versioning = "relaxed")
98+
return strict_ignored_major_versions if semantic_versioning == "strict"
99+
100+
ignored_major_versions
101+
end
102+
77103
sig { returns(T.nilable([Integer, Integer, Integer])) }
78104
def semver_parts
79105
# Extracts only the numeric major.minor.patch part of the version, ensuring it starts with a number
@@ -86,5 +112,56 @@ def semver_parts
86112
major, minor, patch = first_match.split(".").map(&:to_i)
87113
[major || 0, minor || 0, patch || 0]
88114
end
115+
116+
private
117+
118+
# Strict semver: for 0.0.z versions, patch changes are breaking
119+
sig { returns(T::Array[String]) }
120+
def strict_ignored_patch_versions
121+
parts = to_semver.split(".")
122+
major = parts[0].to_i
123+
minor = parts[1].to_i
124+
125+
# For 0.0.z versions, patch changes are breaking, so treat as major
126+
return strict_ignored_major_versions if major.zero? && minor.zero?
127+
128+
ignored_patch_versions
129+
end
130+
131+
# Strict semver: for 0.y.z versions, minor changes are breaking
132+
sig { returns(T::Array[String]) }
133+
def strict_ignored_minor_versions
134+
parts = to_semver.split(".")
135+
major = parts[0].to_i
136+
137+
# For 0.y.z versions, minor changes are breaking, so treat as major
138+
return strict_ignored_major_versions if major.zero?
139+
140+
ignored_minor_versions
141+
end
142+
143+
# Strict semver: for 0.x versions, returns range starting from next breaking version
144+
sig { returns(T::Array[String]) }
145+
def strict_ignored_major_versions
146+
parts = to_semver.split(".")
147+
major = parts[0].to_i
148+
minor = parts[1].to_i
149+
150+
# For 0.0.z versions, any patch change is breaking
151+
if major.zero? && minor.zero?
152+
patch = parts[2].to_i
153+
lower_parts = [0, 0, patch + 1] + [lowest_prerelease_suffix]
154+
return [">= #{lower_parts.join('.')}"]
155+
end
156+
157+
# For 0.y.z versions (y > 0), minor changes are breaking
158+
if major.zero?
159+
lower_parts = [0, minor + 1] + [lowest_prerelease_suffix]
160+
return [">= #{lower_parts.join('.')}"]
161+
end
162+
163+
# For 1.y.z+ versions, use standard semantic versioning
164+
ignored_major_versions
165+
end
89166
end
90167
end

common/spec/dependabot/config/ignore_condition_spec.rb

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,5 +342,124 @@ def expect_ignored(versions)
342342
end
343343
end
344344
end
345+
346+
context "with semantic_versioning option" do
347+
let(:ignore_condition) { described_class.new(dependency_name: dependency_name, update_types: update_types) }
348+
349+
context "with 0.y.z versions" do
350+
let(:dependency_version) { "0.15.5" }
351+
let(:patch_upgrades) { %w(0.15.6 0.15.7) }
352+
let(:minor_upgrades) { %w(0.16.0 0.17.0) }
353+
let(:major_upgrades) { %w(1.0.0 2.0.0) }
354+
355+
context "with ignore_major_versions in relaxed mode" do
356+
subject(:ignored_versions) do
357+
ignore_condition.ignored_versions(dependency, security_updates_only, semantic_versioning: "relaxed")
358+
end
359+
360+
let(:update_types) { ["version-update:semver-major"] }
361+
362+
it "only ignores actual major versions (1.0+)" do
363+
expect(ignored_versions).to eq([">= 1.a"])
364+
end
365+
366+
it "allows minor and patch upgrades within 0.y.z" do
367+
expect_allowed(patch_upgrades + minor_upgrades)
368+
expect_ignored(major_upgrades)
369+
end
370+
end
371+
372+
context "with ignore_major_versions in strict mode" do
373+
subject(:ignored_versions) do
374+
ignore_condition.ignored_versions(dependency, security_updates_only, semantic_versioning: "strict")
375+
end
376+
377+
let(:update_types) { ["version-update:semver-major"] }
378+
379+
it "treats minor bumps in 0.y.z as major" do
380+
expect(ignored_versions).to eq([">= 0.16.a"])
381+
end
382+
383+
it "ignores minor bumps as breaking changes" do
384+
expect_allowed(patch_upgrades)
385+
expect_ignored(minor_upgrades + major_upgrades)
386+
end
387+
end
388+
389+
context "with ignore_minor_versions in strict mode" do
390+
subject(:ignored_versions) do
391+
ignore_condition.ignored_versions(dependency, security_updates_only, semantic_versioning: "strict")
392+
end
393+
394+
let(:update_types) { ["version-update:semver-minor"] }
395+
396+
it "treats 0.y.z minor changes as major, so no minor range exists" do
397+
# In strict mode, minor changes in 0.y.z are major, so ignoring "minor" returns the major range
398+
expect(ignored_versions).to eq([">= 0.16.a"])
399+
end
400+
end
401+
end
402+
403+
context "with 0.0.z versions" do
404+
let(:dependency_version) { "0.0.3" }
405+
let(:patch_upgrades) { %w(0.0.4 0.0.5) }
406+
let(:minor_upgrades) { %w(0.1.0 0.2.0) }
407+
let(:major_upgrades) { %w(1.0.0 2.0.0) }
408+
409+
context "with ignore_major_versions in strict mode" do
410+
subject(:ignored_versions) do
411+
ignore_condition.ignored_versions(dependency, security_updates_only, semantic_versioning: "strict")
412+
end
413+
414+
let(:update_types) { ["version-update:semver-major"] }
415+
416+
it "treats patch bumps in 0.0.z as major" do
417+
expect(ignored_versions).to eq([">= 0.0.4.a"])
418+
end
419+
420+
it "ignores all version bumps as breaking changes" do
421+
expect_ignored(patch_upgrades + minor_upgrades + major_upgrades)
422+
end
423+
end
424+
425+
context "with ignore_patch_versions in strict mode" do
426+
subject(:ignored_versions) do
427+
ignore_condition.ignored_versions(dependency, security_updates_only, semantic_versioning: "strict")
428+
end
429+
430+
let(:update_types) { ["version-update:semver-patch"] }
431+
432+
it "treats 0.0.z patch changes as major, so returns major range" do
433+
# In strict mode, patch changes in 0.0.z are major, so ignoring "patch" returns the major range
434+
expect(ignored_versions).to eq([">= 0.0.4.a"])
435+
end
436+
end
437+
end
438+
439+
context "with 1.y.z versions (standard semver)" do
440+
let(:dependency_version) { "1.2.3" }
441+
let(:update_types) { ["version-update:semver-major"] }
442+
443+
context "with relaxed mode" do
444+
subject(:ignored_versions) do
445+
ignore_condition.ignored_versions(dependency, security_updates_only, semantic_versioning: "relaxed")
446+
end
447+
448+
it "uses standard semver" do
449+
expect(ignored_versions).to eq([">= 2.a"])
450+
end
451+
end
452+
453+
context "with strict mode" do
454+
subject(:ignored_versions) do
455+
ignore_condition.ignored_versions(dependency, security_updates_only, semantic_versioning: "strict")
456+
end
457+
458+
it "uses standard semver (same as relaxed for >= 1.0)" do
459+
expect(ignored_versions).to eq([">= 2.a"])
460+
end
461+
end
462+
end
463+
end
345464
end
346465
end

0 commit comments

Comments
 (0)