Skip to content

Commit c4985d4

Browse files
committed
feat: add bump_per_commit option to control version increment behavior
When bump_per_commit is false, only apply a single +1 increment for the highest bump level across all commits, matching standard semantic-release behavior. Defaults to true to preserve backward compatibility. Closes #11
1 parent ba79c52 commit c4985d4

2 files changed

Lines changed: 95 additions & 0 deletions

File tree

lib/fastlane/plugin/semantic_release/actions/analyze_commits.rb

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,18 @@ def self.get_beginning_of_next_sprint(params)
101101
}
102102
end
103103

104+
def self.clamp_version(next_major, next_minor, next_patch, base_major, base_minor, base_patch)
105+
if next_major > base_major
106+
[base_major + 1, 0, 0]
107+
elsif next_minor > base_minor
108+
[next_major, base_minor + 1, 0]
109+
elsif next_patch > base_patch
110+
[next_major, next_minor, base_patch + 1]
111+
else
112+
[next_major, next_minor, next_patch]
113+
end
114+
end
115+
104116
def self.is_releasable(params)
105117
# Hash of the commit where is the last version
106118
beginning = get_beginning_of_next_sprint(params)
@@ -122,6 +134,11 @@ def self.is_releasable(params)
122134
next_minor = (version.split('.')[1] || 0).to_i
123135
next_patch = (version.split('.')[2] || 0).to_i
124136

137+
# Save base version for potential clamping
138+
base_major = next_major
139+
base_minor = next_minor
140+
base_patch = next_patch
141+
125142
is_next_version_compatible_with_codepush = true
126143

127144
# Get commits log between last version and head
@@ -171,6 +188,11 @@ def self.is_releasable(params)
171188
UI.message("#{next_version}: #{subject}") if params[:show_version_path]
172189
end
173190

191+
# When bump_per_commit is false, clamp to single increment
192+
unless params[:bump_per_commit]
193+
next_major, next_minor, next_patch = clamp_version(next_major, next_minor, next_patch, base_major, base_minor, base_patch)
194+
end
195+
174196
next_version = "#{next_major}.#{next_minor}.#{next_patch}"
175197

176198
is_next_version_releasable = Helper::SemanticReleaseHelper.semver_gt(next_version, version)
@@ -201,6 +223,9 @@ def self.is_codepush_friendly(params)
201223
next_major = 0
202224
next_minor = 0
203225
next_patch = 0
226+
base_major = next_major
227+
base_minor = next_minor
228+
base_patch = next_patch
204229
last_incompatible_codepush_version = '0.0.0'
205230

206231
if hash_lines.to_i > 1
@@ -246,6 +271,10 @@ def self.is_codepush_friendly(params)
246271
end
247272
end
248273

274+
unless params[:bump_per_commit]
275+
next_major, next_minor, next_patch = clamp_version(next_major, next_minor, next_patch, base_major, base_minor, base_patch)
276+
end
277+
249278
Actions.lane_context[SharedValues::RELEASE_LAST_INCOMPATIBLE_CODEPUSH_VERSION] = last_incompatible_codepush_version
250279
end
251280

@@ -353,6 +382,13 @@ def self.available_options
353382
default_value: false,
354383
type: Boolean,
355384
optional: true
385+
),
386+
FastlaneCore::ConfigItem.new(
387+
key: :bump_per_commit,
388+
description: "When true (default), each fix/feat commit increments the version. When false, only bump once per release (matching semantic-release behavior)",
389+
default_value: true,
390+
type: Boolean,
391+
optional: true
356392
)
357393
]
358394
end

spec/analyze_commits_spec.rb

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,65 @@ def execute_lane_test(params)
448448
end
449449
end
450450

451+
describe "bump_per_commit false" do
452+
it "should increment patch only once for multiple fixes" do
453+
commits = [
454+
"fix: first fix|",
455+
"fix: second fix|",
456+
"fix: third fix|"
457+
]
458+
test_analyze_commits(commits)
459+
460+
expect(execute_lane_test(match: 'v*', bump_per_commit: false)).to eq(true)
461+
expect(Fastlane::Actions.lane_context[Fastlane::Actions::SharedValues::RELEASE_NEXT_VERSION]).to eq("1.0.9")
462+
end
463+
464+
it "should increment minor only once for feat + fixes" do
465+
commits = [
466+
"feat: new feature|",
467+
"fix: first fix|",
468+
"fix: second fix|"
469+
]
470+
test_analyze_commits(commits)
471+
472+
expect(execute_lane_test(match: 'v*', bump_per_commit: false)).to eq(true)
473+
expect(Fastlane::Actions.lane_context[Fastlane::Actions::SharedValues::RELEASE_NEXT_VERSION]).to eq("1.1.0")
474+
end
475+
476+
it "should increment minor only once for multiple feats" do
477+
commits = [
478+
"feat: feature one|",
479+
"feat: feature two|"
480+
]
481+
test_analyze_commits(commits)
482+
483+
expect(execute_lane_test(match: 'v*', bump_per_commit: false)).to eq(true)
484+
expect(Fastlane::Actions.lane_context[Fastlane::Actions::SharedValues::RELEASE_NEXT_VERSION]).to eq("1.1.0")
485+
end
486+
487+
it "should increment major only once for breaking + feat + fix" do
488+
commits = [
489+
"fix: ...|BREAKING CHANGE: something",
490+
"feat: ...|",
491+
"fix: ...|"
492+
]
493+
test_analyze_commits(commits)
494+
495+
expect(execute_lane_test(match: 'v*', bump_per_commit: false)).to eq(true)
496+
expect(Fastlane::Actions.lane_context[Fastlane::Actions::SharedValues::RELEASE_NEXT_VERSION]).to eq("2.0.0")
497+
end
498+
499+
it "should return false when there is no releasable change" do
500+
commits = [
501+
"docs: update readme|",
502+
"chore: cleanup|"
503+
]
504+
test_analyze_commits(commits)
505+
506+
expect(execute_lane_test(match: 'v*', bump_per_commit: false)).to eq(false)
507+
end
508+
end
509+
451510
after do
452511
end
453512
end

0 commit comments

Comments
 (0)