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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ _None_

### New Features

- Add the possibility to configure in DerivedBuildCodeFormatter a versioning prefix instead of always defaulting to 1. [#656]
- Add the possibility to configure in `DerivedBuildCodeFormatter` a versioning prefix instead of always defaulting to 1. [#656]
- Add configurable number of digits in version components in `DerivedBuildCodeFormatter`. [#657]

### Bug Fixes

Expand Down
8 changes: 8 additions & 0 deletions lib/fastlane/plugin/wpmreleasetoolkit/models/app_version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ def initialize(major, minor, patch = 0, build_number = 0)
def to_s
"#{@major}.#{@minor}.#{@patch}.#{@build_number}"
end

# Returns an array of the version components.
#
# @return [Array<Integer>] an array of the version components.
#
def components
[@major, @minor, @patch, @build_number]
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,35 @@
module Fastlane
module Wpmreleasetoolkit
module Versioning
# Max total for `*_digits` params, not counting prefix
MAX_TOTAL_DIGITS = 8
MIN_DIGIT_COUNT = 1
MAX_DIGIT_COUNT = 3

# The `DerivedBuildCodeFormatter` class is a specialized build code formatter for derived build codes.
# It takes in an AppVersion object and derives a build code from it.
class DerivedBuildCodeFormatter
# Initialize the formatter with a configurable prefix.
# Initialize the formatter with configurable prefix and digit counts.
#
# @param [String] prefix The prefix to use for the build code. Must be a single digit (0-9), or empty string / nil.
# @param [Integer] major_digits Number of digits for major version. Must be between 1–3. Defaults to 2.
# @param [Integer] minor_digits Number of digits for minor version. Must be between 1–3. Defaults to 2.
# @param [Integer] patch_digits Number of digits for patch version. Must be between 1–3. Defaults to 2.
# @param [Integer] build_digits Number of digits for build number. Must be between 1–3. Defaults to 2.
#
def initialize(prefix: nil)
prefix ||= ''
def initialize(prefix: nil, major_digits: 2, minor_digits: 2, patch_digits: 2, build_digits: 2)
validate_prefix!(prefix)
@prefix = prefix.to_s

@digit_counts = [major_digits, minor_digits, patch_digits, build_digits]
@digit_counts.each { |d| validate_digit_count!(d) }
validate_total_digits!(@digit_counts)
end

# Calculate the next derived build code.
#
# This method derives a new build code from the given AppVersion object by concatenating the configured prefix,
# the major version, the minor version, the patch version, and the build number.
# the major version, the minor version, the patch version, and the build number with configurable digit counts.
#
# @param [AppVersion] version The AppVersion object to derive the next build code from.
#
Expand All @@ -28,19 +40,16 @@ def initialize(prefix: nil)
#
# @return [String] The formatted build code string.
#
def build_code(build_code = nil, version:)
result = format(
# The prefix is configurable to allow for additional platforms or
# extensions that could use a different digit prefix such as 2, etc.
'%<prefix>s%<major>.2i%<minor>.2i%<patch>.2i%<build_number>.2i',
prefix: @prefix,
major: version.major,
minor: version.minor,
patch: version.patch,
build_number: version.build_number
)

result.gsub(/^0+/, '')
def build_code(_build_code = nil, version:)
formatted_components = version.components.zip(@digit_counts).map do |value, width|
comp = value.to_s.rjust(width, '0')
if comp.length > width
UI.user_error!("Version component value (#{value}) exceeds maximum allowed width of #{width} characters. " \
"Consider increasing the corresponding `*_digits` parameter of your `#{self.class.name}`")
end
comp
end
[@prefix, *formatted_components].join.gsub(/^0+/, '')
end

private
Expand All @@ -52,6 +61,10 @@ def build_code(build_code = nil, version:)
# @raise [StandardError] If the prefix is invalid
#
def validate_prefix!(prefix)
unless prefix.nil? || prefix.is_a?(String) || prefix.is_a?(Integer)
UI.user_error!("Prefix must be a string or integer, got: #{prefix.class}")
end

prefix_str = prefix.to_s
Comment on lines 63 to 68
Copy link
Contributor

Choose a reason for hiding this comment

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

👍 Today I re-learned that nil.to_s returns the '' empty string (as opposed to crashing).

This is why even if now you call validate_prefix! from the def initialize without doing the prefix ||= '' anymore first, this prefix_str = prefix.to_s still doesn't crash 👍


# Allow empty string
Expand All @@ -67,6 +80,37 @@ def validate_prefix!(prefix)

UI.user_error!("Prefix must be an integer digit (0-9) or empty string, got: '#{prefix_str}'")
end

# Validates that the digit count is a valid positive integer within reasonable limits.
#
# @param [Integer] digit_count The digit count to validate
#
# @raise [StandardError] If the digit count is invalid
#
def validate_digit_count!(digit_count)
# Check if it's an integer
unless digit_count.is_a?(Integer)
UI.user_error!("Digit count must be an integer, got: #{digit_count.class}")
end

return if digit_count.between?(MIN_DIGIT_COUNT, MAX_DIGIT_COUNT)

UI.user_error!("Digit count must be between #{MIN_DIGIT_COUNT} and #{MAX_DIGIT_COUNT} digits, got: #{digit_count}")
end

# Validates that the total number of digits (excluding prefix) doesn't exceed the maximum for multiplatform compatibility.
#
# Since Google Play's max versionCode is ≈ 2_000_000_000, we want to avoid being too close to the limit
# as this would then block us from submitting any updates for that app if we reached it.
def validate_total_digits!(digits_list)
total_digits = digits_list.sum

# Limit total digits to 8 (excluding prefix)
return if total_digits <= MAX_TOTAL_DIGITS

UI.user_error!("Total digit count (#{total_digits}) exceeds maximum allowed (#{MAX_TOTAL_DIGITS}). " \
"Current config: major(#{digits_list[0]}) + minor(#{digits_list[1]}) + patch(#{digits_list[2]}) + build(#{digits_list[3]}) digits")
end
end
end
end
Expand Down
Loading