Module: Kettle::Gha::Pins::VersionRubric
- Defined in:
- lib/kettle/gha/pins/version_rubric.rb
Overview
Shared version ordering and upgrade selection rules for GitHub Action pins.
Constant Summary collapse
- VALID_UPGRADE_LEVELS =
%w[major minor patch].freeze
- DEFAULT_UPGRADE_LEVEL =
"major"- RELEASE_TAG_RE =
/\A(?:\d+|\d+\.\d+(?:\.\d+)?(?:[-.]?[0-9A-Za-z.-]+)?)\z/
Class Method Summary collapse
- .allowed_by_level?(current_version, current, entry, level) ⇒ Boolean
- .build_release_versions(release_tags:, tag_shas:, release_dates: {}) ⇒ Object
-
.canonical_version_for(resolved_version, known_versions, resolved_sha: nil, comment_version: nil) ⇒ Object
Canonical spellings are version-specific and SHA-aware.
- .canonicalize_equivalent_release_versions(releases) ⇒ Object
- .choose_upgrade_target(current_version, versions, level) ⇒ Object
-
.comment_update_version(comment_version, resolved_version, resolved_sha: nil, known_versions: []) ⇒ Object
The comment on
uses:is a cached record that needs to do two things:. - .entries_for_semantic_version(versions, version_obj) ⇒ Object
- .entry(tag:, sha: nil, released_at: nil) ⇒ Object
- .equivalent_release_tag?(left, right) ⇒ Boolean
- .latest_outdated_target(current_version, versions) ⇒ Object
- .major_line?(value) ⇒ Boolean
- .normalize_upgrade_level(level) ⇒ Object
- .parse(value) ⇒ Object
- .sort_key(entry) ⇒ Object
- .sort_versions(versions) ⇒ Object
- .specificity(entry) ⇒ Object
- .strip_leading_v(text) ⇒ Object
Class Method Details
.allowed_by_level?(current_version, current, entry, level) ⇒ Boolean
181 182 183 184 185 186 187 188 189 190 191 192 193 194 |
# File 'lib/kettle/gha/pins/version_rubric.rb', line 181 def allowed_by_level?(current_version, current, entry, level) return true if level == "major" return false if major_line?(entry.fetch(:version)) return false if major_line?(current_version) case level when "patch" entry.fetch(:version_obj).segments[0, 2] == current.segments[0, 2] when "minor" entry.fetch(:version_obj).segments[0] == current.segments[0] else true end end |
.build_release_versions(release_tags:, tag_shas:, release_dates: {}) ⇒ Object
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
# File 'lib/kettle/gha/pins/version_rubric.rb', line 40 def build_release_versions(release_tags:, tag_shas:, release_dates: {}) = .each_with_object({}) { |tag, memo| memo[tag.to_s] = true } releases = .each_with_object([]) do |tag, memo| release_entry = entry(tag: tag, sha: tag_shas[tag.to_s], released_at: release_dates[tag.to_s]) memo << release_entry if release_entry end tag_versions = tag_shas.each_with_object([]) do |(tag, sha), memo| next if [tag.to_s] tag_entry = entry(tag: tag, sha: sha) memo << tag_entry if tag_entry end sort_versions(canonicalize_equivalent_release_versions(releases + tag_versions)).reverse end |
.canonical_version_for(resolved_version, known_versions, resolved_sha: nil, comment_version: nil) ⇒ Object
Canonical spellings are version-specific and SHA-aware.
- For SHA-tracked tags, prefer the most explicit spelling that resolves to the same SHA as the currently resolved target.
- If the resolved SHA is known but the currently-commented spellings are tied to a different SHA, keep the in-file spelling when the semantic version is unchanged, so we don't rewrite between aliases that no longer represent the same object.
- Otherwise fall back to the resolved version form.
144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 |
# File 'lib/kettle/gha/pins/version_rubric.rb', line 144 def canonical_version_for(resolved_version, known_versions, resolved_sha: nil, comment_version: nil) resolved = parse(resolved_version) return resolved_version.to_s unless resolved equivalents = entries_for_semantic_version(known_versions, resolved) if !resolved_sha.to_s.empty? sha_matched_equivalents = equivalents.select do |entry| entry.fetch(:sha).to_s == resolved_sha.to_s end equivalents = sha_matched_equivalents unless sha_matched_equivalents.empty? if sha_matched_equivalents.empty? && comment_version && parse(comment_version) == resolved return strip_leading_v(comment_version) end end preferred = equivalents.max_by { |entry| sort_key(entry) } preferred ? preferred[:version] : resolved.to_s end |
.canonicalize_equivalent_release_versions(releases) ⇒ Object
56 57 58 59 60 61 |
# File 'lib/kettle/gha/pins/version_rubric.rb', line 56 def canonicalize_equivalent_release_versions(releases) releases.each_with_object([]) do |release, groups| group = groups.find { |entries| equivalent_release_tag?(entries.first, release) } group ? group << release : groups << [release] end.map { |entries| entries.max_by { |entry| sort_key(entry) } } end |
.choose_upgrade_target(current_version, versions, level) ⇒ Object
84 85 86 87 88 89 90 91 92 93 94 95 96 |
# File 'lib/kettle/gha/pins/version_rubric.rb', line 84 def choose_upgrade_target(current_version, versions, level) normalized_level = normalize_upgrade_level(level) current = parse(current_version) return nil if current.nil? return nil if normalized_level != "major" && major_line?(current_version) versions.select do |entry| entry.fetch(:version_obj, nil).is_a?(Gem::Version) && entry.fetch(:version_obj) > current && (!entry.fetch(:version_obj).prerelease? || current.prerelease?) && allowed_by_level?(current_version, current, entry, normalized_level) end.max_by { |entry| sort_key(entry) } end |
.comment_update_version(comment_version, resolved_version, resolved_sha: nil, known_versions: []) ⇒ Object
The comment on uses: is a cached record that needs to do two things:
- Track the currently resolved target (if the target changes, update the comment even when both values parse to version objects).
- Normalize equivalent spellings (v2, v2.0, v2.0.0...) to the most specific canonical form available for that SHA in known_versions.
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
# File 'lib/kettle/gha/pins/version_rubric.rb', line 115 def comment_update_version(comment_version, resolved_version, resolved_sha: nil, known_versions: []) comment = parse(comment_version) resolved = parse(resolved_version) return nil unless comment && resolved canonical = canonical_version_for( resolved_version, known_versions, resolved_sha: resolved_sha, comment_version: comment_version ) return nil if comment_version.to_s == canonical canonical end |
.entries_for_semantic_version(versions, version_obj) ⇒ Object
131 132 133 |
# File 'lib/kettle/gha/pins/version_rubric.rb', line 131 def entries_for_semantic_version(versions, version_obj) versions.select { |entry| entry.fetch(:version_obj, nil) == version_obj } end |
.entry(tag:, sha: nil, released_at: nil) ⇒ Object
27 28 29 30 31 32 33 34 35 36 37 38 |
# File 'lib/kettle/gha/pins/version_rubric.rb', line 27 def entry(tag:, sha: nil, released_at: nil) version_obj = parse(tag) return nil unless version_obj { tag: tag.to_s, version_obj: version_obj, version: version_obj.to_s, sha: sha, released_at: released_at.to_s } end |
.equivalent_release_tag?(left, right) ⇒ Boolean
174 175 176 177 178 179 |
# File 'lib/kettle/gha/pins/version_rubric.rb', line 174 def equivalent_release_tag?(left, right) left[:version_obj] == right[:version_obj] && left[:sha] && right[:sha] && left[:sha] == right[:sha] end |
.latest_outdated_target(current_version, versions) ⇒ Object
98 99 100 101 102 103 104 105 106 107 |
# File 'lib/kettle/gha/pins/version_rubric.rb', line 98 def latest_outdated_target(current_version, versions) current = parse(current_version) return nil if current.nil? versions.select do |entry| entry.fetch(:version_obj, nil).is_a?(Gem::Version) && entry.fetch(:version_obj) > current && (!entry.fetch(:version_obj).prerelease? || current.prerelease?) end.max_by { |entry| sort_key(entry) } end |
.major_line?(value) ⇒ Boolean
23 24 25 |
# File 'lib/kettle/gha/pins/version_rubric.rb', line 23 def major_line?(value) value.to_s.match?(/\A\d+\z/) end |
.normalize_upgrade_level(level) ⇒ Object
169 170 171 172 |
# File 'lib/kettle/gha/pins/version_rubric.rb', line 169 def normalize_upgrade_level(level) normalized = level.to_s.downcase VALID_UPGRADE_LEVELS.include?(normalized) ? normalized : DEFAULT_UPGRADE_LEVEL end |
.parse(value) ⇒ Object
14 15 16 17 18 19 20 21 |
# File 'lib/kettle/gha/pins/version_rubric.rb', line 14 def parse(value) normalized = value.to_s.sub(/\A[vV]/, "") return nil unless normalized.match?(RELEASE_TAG_RE) Gem::Version.new(normalized) rescue ArgumentError nil end |
.sort_key(entry) ⇒ Object
67 68 69 |
# File 'lib/kettle/gha/pins/version_rubric.rb', line 67 def sort_key(entry) [entry.fetch(:version_obj), *specificity(entry)] end |
.sort_versions(versions) ⇒ Object
63 64 65 |
# File 'lib/kettle/gha/pins/version_rubric.rb', line 63 def sort_versions(versions) versions.sort_by { |entry| sort_key(entry) } end |
.specificity(entry) ⇒ Object
71 72 73 74 75 76 77 78 79 80 81 82 |
# File 'lib/kettle/gha/pins/version_rubric.rb', line 71 def specificity(entry) text = entry.fetch(:tag).to_s.sub(/\A[vV]/, "") release_text, suffix = text.split(/[-.](?=[A-Za-z])/, 2) numeric_segments = release_text.to_s.split(".").take_while { |part| part.match?(/\A\d+\z/) } [ numeric_segments.length, suffix ? 0 : 1, text.length, entry.fetch(:tag).to_s ] end |
.strip_leading_v(text) ⇒ Object
165 166 167 |
# File 'lib/kettle/gha/pins/version_rubric.rb', line 165 def strip_leading_v(text) text.to_s.sub(/\A[vV]/, "") end |