-
Notifications
You must be signed in to change notification settings - Fork 9
[WIP] Automatic Version Numbering #350
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
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,:patchand:prerelease. We could even consider directly using the built-inGem::Versionclass for that - Have a (set of sub-)class(es) dedicated to parsing and formatting
Versionobjects from and toStringrepresentations, based on the formatting convention used by the project- For example, we'd have an abstract class
VersionFormatterthat would expect methods likedef parse(string:)taking aStringand returning aVersionmodel, adef read_from_fileandwrite_to_file, anddef to_build_number(version:)anddef to_version_name(version:), taking aVersionmodel and returning aString(typically to be used as theversionCode/VERSION_SHORTand theversionName/VERSION_LONGfor iOS/Android) - Then we'd provide subclasses of that abstract class for the various formats/conventions we want to support.
- For example
class AndroidVersionFormatter < VersionFormatterwould know how to parse and format versions using thex.y.z-rc-nconvention,load_from_filewould read the version from a.propertiesfile, andto_build_numberwould generate a string like like1xxyyzznn - While
class IOSVersionFormatterwould maybe usex.y.z.nformat instead,load_from_filewould use theXcodeproj::Configclass to read it from.xcconfigfile, etc - And we could even imagine a
class DateVersionFormatterfor 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: 0parameter to allow to start the next minor version at0or 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_fileto get aVersionmodel 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-nversionName - 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_nameanddef ios_version_number, then allowing an Android client to callios_version_numbereven 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
Fastfileonce and for all.- Then the lane would just have to use
formatter.load_from_fileorbumper.bump_minorwhen 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_codeprovide 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-nformat tox.y.z.nformat on a given project), we'd just have to change theVersionFormattersubclass 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.yx.y.zx.y.z.nx.y.z-rcnx.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
endWe 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.