-
Notifications
You must be signed in to change notification settings - Fork 9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WIP] Automatic Version Numbering #350
base: trunk
Are you sure you want to change the base?
Conversation
392e4af
to
56b50b8
Compare
56b50b8
to
4343826
Compare
tags = @github_client.tags(repository) | ||
|
||
# GitHub Enterprise can return raw HTML if the connection isn't | ||
#working, so we need to validate that this is what we expect it is |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚫 | Missing space after # . |
|
||
# GitHub Enterprise can return raw HTML if the connection isn't | ||
#working, so we need to validate that this is what we expect it is | ||
UI.crash! 'Unable to connect to GitHub. Please try again later.' if !tags.is_a? Array |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚫 | Favor unless over if for negative conditions. |
UI.crash! 'Unable to connect to GitHub. Please try again later.' if !tags.is_a? Array | ||
|
||
tags.map { |t| Version.create(t[:name]) } | ||
.compact |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚫 | Align .compact with .map on line 55. |
|
||
tags.map { |t| Version.create(t[:name]) } | ||
.compact | ||
.filter { |v| v.is_different_rc_of(version) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚫 | Align .filter with .map on line 55. |
tags.map { |t| Version.create(t[:name]) } | ||
.compact | ||
.filter { |v| v.is_different_rc_of(version) } | ||
.filter(&:prerelease?) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚫 | Align .filter with .map on line 55. |
.compact | ||
.filter { |v| v.is_different_rc_of(version) } | ||
.filter(&:prerelease?) | ||
.sort |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚫 | Align .sort with .map on line 55. |
.filter { |v| v.is_different_rc_of(version) } | ||
.filter(&:prerelease?) | ||
.sort | ||
.reverse |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚫 | Align .reverse with .map on line 55. |
.filter(&:prerelease?) | ||
.sort | ||
.reverse | ||
.first |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚫 | Align .first with .map on line 55. |
Haven't taken the time to read that code yet (it's still in Draft after all, and there's no PR description yet to explain the idea behind 😛 ) but could this go in the direction of what I had in mind in #203? Or is it about something completely different? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@spencertransier Sorry for taking so long to reply to your Slack request and finally check this PR 😅
I think the Version
model could be simplified a lot, and that we should separate responsibilities between objects — in particular I feel like VersionHelper
is doing a lot.
[Spoiler: Sorry for the long wall of text ahead; but since you also asked for high-level architecture feedback, I figured you'd be interested in all my rationale and ideas for the bigger picture]
My personal take on how we should probably architecture this would be:
- Have a simple "bag of properties" model, which would basically be a
class
(or even just aStruct.new
) with only 4 properties:major
,:minor
,:patch
and:prerelease
. We could even consider directly using the built-inGem::Version
class for that - Have a (set of sub-)class(es) dedicated to parsing and formatting
Version
objects from and toString
representations, based on the formatting convention used by the project- For example, we'd have an abstract class
VersionFormatter
that would expect methods likedef parse(string:)
taking aString
and returning aVersion
model, adef read_from_file
andwrite_to_file
, anddef to_build_number(version:)
anddef to_version_name(version:)
, taking aVersion
model and returning aString
(typically to be used as theversionCode
/VERSION_SHORT
and theversionName
/VERSION_LONG
for iOS/Android) - Then we'd provide subclasses of that abstract class for the various formats/conventions we want to support.
- For example
class AndroidVersionFormatter < VersionFormatter
would know how to parse and format versions using thex.y.z-rc-n
convention,load_from_file
would read the version from a.properties
file, andto_build_number
would generate a string like like1xxyyzznn
- While
class IOSVersionFormatter
would maybe usex.y.z.n
format instead,load_from_file
would use theXcodeproj::Config
class to read it from.xcconfig
file, etc - And we could even imagine a
class DateVersionFormatter
for DayOne-style strings or any other subclasses as needed if we need to support more formats
- For example, we'd have an abstract class
- Have a class that we could name
VersionBumper
, dedicated to know how to bump versions given a "bumping rule" used by the project. For examplevb = VersionBumper.new(maxMinor: 12)
would be suitable for date-based version format conventions whilevb = VersionBumper.new(maxMinor: 9)
would be used for WP/WC which bump the major version by one after they reached minor version 9. And if needed, this class could also accept other constraints, likeminPatch: 0
parameter to allow to start the next minor version at0
or at1
, etc
With that in place, each client project would:
- Configure e.g.
vb = VersionBumper.new(maxMinor: 9)
, as well as e.g.vf = AndroidVersionFormatter.new(config_file: 'versions.properties', version_code_prefix: 1)
, at the top of theirFastfile
, to declare their overall bumping convention and formatter to be used/shared by all the lanes of that project - Then when they need to read the current version, they'd use
v = vf.load_from_file
to get aVersion
model instance, thenvf.to_build_number(v)
to format it as a Build Number orvf.to_version_name(v)
to format it as ax.y.z-rc-n
versionName - If we need to bump the version in the version file,
v = vf.load_from_file; v = vb.bump_minor(v); vf.write_to_file(v)
would do the trick - etc.
IMHO separating those concerns and responsibilities this way, with one "dump" model and two separate classes for formatting vs bumping, has many advantages compared to trying to do everything in a big VersionHelper
or by trying to implement formatting within the Version
model like has been done here.
The main benefit are that:
- This makes it very flexible, allowing to pick and combine the right helpers (formatter + bumper classes) for the project
- as opposed to having the model object implement many different methods like
def android_version_name
anddef ios_version_number
, then allowing an Android client to callios_version_number
even if it wouldn't make sense in the context of an Android project, etc.
- as opposed to having the model object implement many different methods like
- The "rules and conventions" used by the project can be declared in one place, at the top of the
Fastfile
once and for all.- Then the lane would just have to use
formatter.load_from_file
orbumper.bump_minor
when then need to perform those operations, without having to repeat at each call site which convention to use - As opposed to having to repeat the path to the version file at every call site, or to make sure that all lanes call that call
android_version_code
provide the sameprefix:
value consistently, …
- Then the lane would just have to use
- This also means that in the implementation of your lanes, you won't need to even know or account for which convention is being used by the project.
- The calls in your lane will look like
v = vf.load_from_file
,vb.bump_minor(v)
… regardless of the convention used by the project - Which means you can easily copy/paste similar lanes from one project to another (e.g. from DOiOS to DOAndroid), even if those projects use a different version format and bumping convention. Because the convention used will be declared at the top level, not in each lane's implementation
- That also means that if we want to change the version convention at some point (e.g. go from
x.y.z-rc-n
format tox.y.z.n
format on a given project), we'd just have to change theVersionFormatter
subclass used at the top of theFastfile
. Just one line to change—instead of having to go thru all the lanes and have to think and check what we'd need to update
- The calls in your lane will look like
Anyway, this is my personal take and ideas about the architecture that would make things the most flexible for us, but it's also just my personal opinion after all, so feel free to challenge it or play around with the ideas.
# | ||
def self.create(string) | ||
string = string.downcase | ||
string = string.delete_prefix('v') if string.start_with?('v') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
delete_prefix
already checks if the string starts with said prefix (and is a no-op if it doesn't)
string = string.delete_prefix('v') if string.start_with?('v') | |
string = string.delete_prefix('v') |
components = string | ||
.split('.') | ||
.map { |component| component.remove('-') } | ||
.delete_if { |component| component == 'rc' } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The doc says that the method is supposed to handle format like x.y.zrc1
too, but this logic won't cover for those cases. To be honest, I'm not convinced that we should support those x.y.zrc1
format in the first place anyway, so maybe just update the method YARD doc comment above instead?
I think the formats we need to support are:
x.y
x.y.z
x.y.z.n
x.y.z-rcn
x.y.z-rc-n
But probably not much more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Most of the functionality in this model can be implemented by re-using the existing classes available in ruby to manage versions, like Gem::Version
.
For example:
def initialize(major:, minor:, patch: nil, rc_number: nil)
version_string = [major, minor, patch, rc_number].compact.join('.')
@parts = Gem::Version.new(version_string).canonical_segments
end
def self.create(string)
# Dashes are considered to mean 'pre' so '1.2.3-rc-4' would give segments [1,2,3,'pre','rc','pre',4]
@parts = Gem::Version.new(version_string).canonical_segments.reject { |part| part == 'pre' || part == 'rc' }
end
# Return version in format `x.y.z.n`
def to_s
@parts.join('.')
end
# Return version informat `x.y-rc-n`, or `x.y.z-rc-n` if `z>0`
def android_version_name
xyz = @parts[2].zero? ? @parts[0..1] : @parts[0..2] # Do not include patch if zero
rc_suffix = parts[3].nil? ? '' : "-rc-#{parts[3]}"
xyz.join('.') + rc_suffix
end
We could also consider using Gem::Version
's build-in comparison methods to compare two versions
if minor == 9 | ||
major += 1 | ||
minor = 0 | ||
else | ||
minor += 1 | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This works for client repos that use that convention that minor versions loop to the next major after 9. But this won't work for e.g. client repos that use date-based versions (like DayOne) where we'd probably loop after 12 instead, or if one day we have a library or tool repo for which we want to use those release-toolkit actions but for which we don't have that constraint of limiting the minor
version to <10 (e.g. release-toolkit
or dangermattic
or similar projects follow SemVer.org semantics so their minor version can be arbitrary high as long as there's no breaking change happening.
No description provided.