-
Notifications
You must be signed in to change notification settings - Fork 1.5k
helm: support versioning-strategy (range-preserving updates) #15218
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
Merged
robaiken
merged 70 commits into
dependabot:main
from
casey-robertson-paypal:casey/helm-versioning-strategy
Jul 10, 2026
Merged
Changes from 67 commits
Commits
Show all changes
70 commits
Select commit
Hold shift + click to select a range
024a8a5
Add Helm::Requirement for SemVer-range constraint parsing
casey-robertson-paypal d3498ca
Add Helm RequirementsUpdater for versioning-strategy
casey-robertson-paypal 1dfc5bc
Honor versioning-strategy in helm update checker and chart updater
casey-robertson-paypal eb38dd7
helm: support multi-comparator range constraints + expand strategy tests
casey-robertson-paypal be6cf17
helm: address PR review (nil requirement, OR-range satisfaction, anchβ¦
casey-robertson-paypal 898ec96
helm: fix Sorbet static type errors
casey-robertson-paypal c8907e5
helm: harden requirement regexes against polynomial ReDoS
casey-robertson-paypal 527a9f4
helm: correct misleading comment on default strategy behavior
casey-robertson-paypal 6a19336
Merge branch 'main' into casey/helm-versioning-strategy
casey-robertson-paypal dc39b32
Merge branch 'main' into casey/helm-versioning-strategy
casey-robertson-paypal 1f4e4b7
Merge branch 'main' into casey/helm-versioning-strategy
casey-robertson-paypal a32691c
Merge branch 'main' into casey/helm-versioning-strategy
casey-robertson-paypal 2b69b14
Merge branch 'main' into casey/helm-versioning-strategy
casey-robertson-paypal 4352acc
Merge branch 'main' into casey/helm-versioning-strategy
casey-robertson-paypal c8a9f06
Merge branch 'main' into casey/helm-versioning-strategy
casey-robertson-paypal 1ac775e
Merge branch 'main' into casey/helm-versioning-strategy
casey-robertson-paypal 36a0fa0
Merge branch 'main' into casey/helm-versioning-strategy
casey-robertson-paypal ad53a5d
Merge branch 'main' into casey/helm-versioning-strategy
casey-robertson-paypal d7f77e6
Merge upstream/main into casey/helm-versioning-strategy
casey-robertson-paypal 0d8537e
helm: exclude requirements_updater from Sorbet/ForbidTUntyped
casey-robertson-paypal bb88db6
Merge branch 'main' into casey/helm-versioning-strategy
casey-robertson-paypal b550d2a
Merge branch 'main' into casey/helm-versioning-strategy
casey-robertson-paypal 8157959
Merge branch 'main' into casey/helm-versioning-strategy
casey-robertson-paypal 7f598b1
Merge branch 'main' into casey/helm-versioning-strategy
casey-robertson-paypal 3a4f20e
Merge branch 'main' into casey/helm-versioning-strategy
casey-robertson-paypal b3ce984
Merge branch 'main' into casey/helm-versioning-strategy
casey-robertson-paypal f0ef728
Merge branch 'main' into casey/helm-versioning-strategy
casey-robertson-paypal d78e58b
Merge branch 'main' into casey/helm-versioning-strategy
casey-robertson-paypal fd49edd
Merge branch 'main' into casey/helm-versioning-strategy
casey-robertson-paypal 5dfa210
Merge branch 'main' into casey/helm-versioning-strategy
casey-robertson-paypal 9fc4881
Merge branch 'main' into casey/helm-versioning-strategy
casey-robertson-paypal 9d67e91
helm: type requirements as DependencyRequirement (drop T.untyped)
casey-robertson-paypal 642ea82
Merge remote-tracking branch 'upstream/main' into casey/helm-versioniβ¦
casey-robertson-paypal 0d2e0f0
helm: drop ForbidTUntyped exclude; build DependencyRequirement in specs
casey-robertson-paypal f216820
Merge branch 'casey/helm-versioning-strategy' of https://github.com/cβ¦
casey-robertson-paypal b6ea46c
Merge branch 'main' into casey/helm-versioning-strategy
casey-robertson-paypal b08220d
Merge branch 'main' into casey/helm-versioning-strategy
casey-robertson-paypal 2bf3e86
Merge branch 'main' into casey/helm-versioning-strategy
casey-robertson-paypal ce1c3a6
Merge branch 'main' into casey/helm-versioning-strategy
casey-robertson-paypal fd558b4
Merge branch 'main' into casey/helm-versioning-strategy
casey-robertson-paypal aeb50ac
Merge branch 'main' into casey/helm-versioning-strategy
casey-robertson-paypal be942cd
helm: re-trigger CI (GitHub API rate-limit flake on prior run)
casey-robertson-paypal 0b01d0c
Merge branch 'casey/helm-versioning-strategy' of https://github.com/cβ¦
casey-robertson-paypal 836cc42
Merge branch 'main' into casey/helm-versioning-strategy
casey-robertson-paypal 550882d
helm: annotate current_version override; document ClassLength disable
casey-robertson-paypal 60b5b1d
Merge branch 'casey/helm-versioning-strategy' of https://github.com/cβ¦
casey-robertson-paypal 1a02e86
Merge branch 'main' into casey/helm-versioning-strategy
casey-robertson-paypal 67ef423
Merge branch 'main' into casey/helm-versioning-strategy
casey-robertson-paypal b146023
Merge branch 'main' into casey/helm-versioning-strategy
casey-robertson-paypal 4aea043
helm: don't report updates for in-range comparator ranges
casey-robertson-paypal 1624dbf
helm: widen OR ranges; gate can_update? on constraint actually changing
casey-robertson-paypal 35b065b
Merge remote-tracking branch 'upstream/main' into casey/helm-versioniβ¦
casey-robertson-paypal e4becf0
Merge branch 'main' into casey/helm-versioning-strategy
casey-robertson-paypal 2ccbd55
helm: support build metadata / digest suffixes in constraints
casey-robertson-paypal 6c35603
Merge branch 'casey/helm-versioning-strategy' of https://github.com/cβ¦
casey-robertson-paypal 09fcdff
helm: add round-trip invariant spec for requirement parsing
casey-robertson-paypal 116a77a
helm: fix upper-only range anchor, partial hyphen bound, != exclusion
casey-robertson-paypal 59de967
helm: per-occurrence constraints, strict > exclusion, upper-only anchor
casey-robertson-paypal 1b5fe3c
helm: commit duplicate fixture; fix uppercase X wildcard, != anchor, β¦
casey-robertson-paypal 32cfbc4
helm: re-trigger CI (triage/labeler HttpError was a transient GitHub β¦
casey-robertson-paypal d894516
Merge branch 'main' into casey/helm-versioning-strategy
casey-robertson-paypal 2bb8f66
helm: fix comparator+wildcard constraints and comma-AND upper-bound sβ¦
casey-robertson-paypal 6a0940a
Merge branch 'casey/helm-versioning-strategy' of https://github.com/cβ¦
casey-robertson-paypal 69748cd
helm: fix crashes/corruption, simplify anchor, harden multi-occurrence
casey-robertson-paypal 4f65fa2
helm: anchor OR ranges with an unbounded branch at 0; test anchor viaβ¦
casey-robertson-paypal 281fae4
helm: anchor range upper-bound substitution to its comparator
casey-robertson-paypal c03e00d
helm: anchor OR-of-exact-pins on the lowest pin, not 0
casey-robertson-paypal aff88dc
helm: surface unmatched chart rewrites instead of silently skipping
casey-robertson-paypal db0f7d7
helm: anchor multi-occurrence charts on the lowest occurrence
casey-robertson-paypal c9261f8
Merge branch 'main' into casey/helm-versioning-strategy
robaiken File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,218 @@ | ||
| # typed: strict | ||
| # frozen_string_literal: true | ||
|
|
||
| require "sorbet-runtime" | ||
|
|
||
| require "dependabot/requirement" | ||
| require "dependabot/utils" | ||
| require "dependabot/helm/version" | ||
|
|
||
| module Dependabot | ||
| module Helm | ||
| # Parses Helm Chart.yaml SemVer range constraints (^, ~, x-ranges, hyphen | ||
| # ranges, || OR, space-AND and comma-AND). Adapted from | ||
| # Dependabot::NpmAndYarn::Requirement: Helm uses the same SemVer family | ||
| # (Masterminds), unlike the Gem-style Docker::Requirement registered for the | ||
| # docker-image path. | ||
| # | ||
| # NOTE: this class is intentionally NOT registered as the requirement class | ||
| # for "helm" (that remains Docker::Requirement, used by the docker-image | ||
| # path and ignore conditions). It is used explicitly by the range-preserving | ||
| # requirements updater. | ||
| class Requirement < Dependabot::Requirement | ||
| extend T::Sig | ||
|
|
||
| AND_SEPARATOR = T.let(/(?<=[a-zA-Z0-9*])\s+(?:&+\s+)?(?!\s*[|-])/, Regexp) | ||
| OR_SEPARATOR = T.let(/(?<=[a-zA-Z0-9*])\s*\|+/, Regexp) | ||
|
|
||
| # Allow an optional 'v' prefix and an optional '+<build metadata/digest>' | ||
| # suffix. Helm::Version supports the latter (e.g. "1.0.119807+<digest>" | ||
| # from OCI charts), so the requirement parser must accept it too or | ||
| # constraints containing it raise BadRequirementError. | ||
| quoted = OPS.keys.map { |k| Regexp.quote(k) }.join("|") | ||
| version_pattern = "v?#{Gem::Version::VERSION_PATTERN}(?:\\+[0-9A-Za-z\\-.]+)?" | ||
|
|
||
| PATTERN_RAW = T.let("\\s*(#{quoted})?\\s*(#{version_pattern})\\s*".freeze, String) | ||
| PATTERN = T.let(/\A#{PATTERN_RAW}\z/, Regexp) | ||
|
|
||
| # Always returns a Helm::Version (never the plain Gem::Version-backed | ||
| # DefaultRequirement), so satisfaction comparisons stay Helm::Version vs | ||
| # Helm::Version β Helm::Version#<=> has a strict sig that rejects plain | ||
| # Gem::Version operands. | ||
| sig do | ||
| params(obj: T.any(String, Gem::Version)) | ||
| .returns(T::Array[T.any(String, Dependabot::Version)]) | ||
| end | ||
| def self.parse(obj) | ||
| return ["=", Helm::Version.new(obj.to_s)] if obj.is_a?(Gem::Version) | ||
|
|
||
| unless (matches = PATTERN.match(obj.to_s)) | ||
| raise BadRequirementError, "Illformed requirement [#{obj.inspect}]" | ||
| end | ||
|
|
||
| [matches[1] || "=", Helm::Version.new(T.must(matches[2]))] | ||
| end | ||
|
|
||
| # Returns an array of requirements. At least one requirement from the | ||
| # returned array must be satisfied for a version to be valid. | ||
| sig { override.params(requirement_string: T.nilable(String)).returns(T::Array[Requirement]) } | ||
| def self.requirements_array(requirement_string) | ||
| return [new(nil)] if requirement_string.nil? | ||
|
|
||
| # Parentheses are extremely rare in Helm constraints; strip them. | ||
| requirement_string = requirement_string.gsub(/[()]/, "") | ||
| requirement_string.strip.split(OR_SEPARATOR).map do |req_string| | ||
| new(req_string.strip.split(AND_SEPARATOR)) | ||
| end | ||
|
casey-robertson-paypal marked this conversation as resolved.
|
||
| end | ||
|
|
||
| sig { params(requirements: T.nilable(T.any(String, T::Array[String]))).void } | ||
| def initialize(*requirements) | ||
| requirements = requirements.flatten.compact | ||
| # `new(nil)` (from `requirements_array(nil)`) means "match anything". | ||
| # Use a literal ">= 0" so it flows through our `parse` and is backed by a | ||
| # Helm::Version β Gem's DefaultRequirement uses a plain Gem::Version, which | ||
| # Helm::Version#<=>'s strict sig would reject. | ||
| requirements = [">= 0"] if requirements.empty? | ||
|
|
||
| requirements = requirements.flat_map { |req_string| req_string.split(",").map(&:strip) } | ||
| .flat_map { |req_string| convert_helm_constraint_to_ruby_constraint(req_string) } | ||
|
|
||
| super | ||
| end | ||
|
casey-robertson-paypal marked this conversation as resolved.
|
||
|
|
||
| private | ||
|
|
||
| sig { params(req_string: String).returns(T.any(String, T::Array[String])) } | ||
| def convert_helm_constraint_to_ruby_constraint(req_string) | ||
| # Leave dist-tags / non-numeric leading tokens untouched. | ||
| return req_string if req_string.match?(/^([A-Za-uw-z]|v[^\d])/) | ||
|
|
||
| # Wildcards combined with an operator need dedicated handling before the | ||
| # generic wildcard strip below (which would corrupt them). | ||
| wildcard = wildcard_ruby_constraint(req_string) | ||
| return wildcard if wildcard | ||
|
|
||
| dispatch_ruby_constraint(req_string.gsub(/(?:\.|^)[xX*]/, "")) | ||
| end | ||
|
|
||
| # Dispatches a wildcard-stripped constraint token to its converter. | ||
| sig { params(req_string: String).returns(T.any(String, T::Array[String])) } | ||
| def dispatch_ruby_constraint(req_string) | ||
| if req_string.empty? then ">= 0" | ||
| elsif req_string.start_with?("~>") then req_string | ||
| elsif req_string.start_with?("=") then req_string.gsub(/^=*/, "") | ||
| elsif req_string.start_with?("~") then convert_tilde_req(req_string) | ||
| elsif req_string.start_with?("^") then convert_caret_req(req_string) | ||
| elsif req_string.include?(" - ") then convert_hyphen_req(req_string) | ||
| elsif req_string.match?(/[<>]/) then req_string | ||
|
casey-robertson-paypal marked this conversation as resolved.
|
||
| else ruby_range(req_string) | ||
| end | ||
| end | ||
|
|
||
| # Handles wildcard constraints that a leading operator would otherwise | ||
| # corrupt. Returns nil when there is no operator+wildcard to expand. | ||
| # - "<=1.x"/">1.2.x": expand from the wildcard's [floor, ceiling) span | ||
| # (stripping the wildcard first would turn "<=1.x" into "<=1", which | ||
| # wrongly rejects 1.5.0 that Masterminds accepts). | ||
| # - "*"/"^*"/"~*": a bare wildcard means "any version"; any other | ||
| # conversion yields an empty bound. | ||
| sig { params(req_string: String).returns(T.nilable(String)) } | ||
| def wildcard_ruby_constraint(req_string) | ||
| if (m = req_string.match(/\A(<=|>=|<|>)\s*(\d+(?:\.\d+)*)\.[xX*]\z/)) | ||
| return convert_wildcard_comparator(T.must(m[1]), T.must(m[2])) | ||
| end | ||
|
|
||
| return ">= 0" if req_string.match?(/\A[\^~<>=\s]*[xX*]+\z/) | ||
|
casey-robertson-paypal marked this conversation as resolved.
|
||
|
|
||
| nil | ||
| end | ||
|
|
||
| # Expands a comparator applied to a wildcard version ("1.x" spans | ||
| # [1.0.0, 2.0.0); "1.2.x" spans [1.2.0, 1.3.0)) per Masterminds semantics: | ||
| # >= X.x -> >= floor > X.x -> >= ceiling | ||
| # < X.x -> < floor <= X.x -> < ceiling | ||
| sig { params(operator: String, prefix: String).returns(String) } | ||
| def convert_wildcard_comparator(operator, prefix) | ||
| parts = prefix.split(".").map(&:to_i) | ||
| floor = (parts + [0, 0, 0]).first(3) | ||
| ceiling_parts = parts.dup | ||
| ceiling_parts[-1] = T.must(ceiling_parts[-1]) + 1 | ||
| ceiling = (ceiling_parts + [0, 0, 0]).first(3) | ||
|
|
||
| case operator | ||
| when ">=" then ">= #{floor.join('.')}" | ||
| when ">" then ">= #{ceiling.join('.')}" | ||
| when "<" then "< #{floor.join('.')}" | ||
| else "< #{ceiling.join('.')}" # "<=" | ||
| end | ||
| end | ||
|
|
||
| sig { params(req_string: String).returns(String) } | ||
| def convert_tilde_req(req_string) | ||
| version = req_string.gsub(/^~\>?[\s=]*/, "") | ||
| parts = version.split(".") | ||
| parts << "0" if parts.count < 3 | ||
| "~> #{parts.join('.')}" | ||
| end | ||
|
casey-robertson-paypal marked this conversation as resolved.
|
||
|
|
||
| sig { params(req_string: String).returns(T::Array[String]) } | ||
| def convert_hyphen_req(req_string) | ||
| parts = req_string.split(/\s+-\s+/) | ||
| lower_bound = T.must(parts[0]) | ||
| upper_bound = T.must(parts[1]) | ||
| lower_bound_parts = lower_bound.split(".") | ||
| lower_bound_parts.fill("0", lower_bound_parts.length...3) | ||
|
|
||
| upper_bound_parts = upper_bound.split(".") | ||
| upper_bound_range = | ||
| if upper_bound_parts.length < 3 | ||
| # When upper bound is a partial version treat these as an X-range: | ||
| # "1.0 - 2.0" includes the whole 2.0.x series, so always increment | ||
| # the last specified component (even when it is 0) before padding β | ||
| # otherwise the bound becomes "< 2.0.0.a" and excludes 2.0.0. | ||
| upper_bound_parts[-1] = (T.must(upper_bound_parts[-1]).to_i + 1).to_s | ||
| upper_bound_parts.fill("0", upper_bound_parts.length...3) | ||
| "< #{upper_bound_parts.join('.')}.a" | ||
| else | ||
| "<= #{upper_bound_parts.join('.')}" | ||
| end | ||
|
casey-robertson-paypal marked this conversation as resolved.
|
||
|
|
||
| [">= #{lower_bound_parts.join('.')}", upper_bound_range] | ||
| end | ||
|
|
||
| sig { params(req_string: String).returns(String) } | ||
| def ruby_range(req_string) | ||
| parts = req_string.split(".") | ||
| # If we have three or more parts then this is an exact match | ||
| return req_string if parts.count >= 3 | ||
|
|
||
| # If we have fewer than three parts we do a partial match | ||
| parts << "0" | ||
| "~> #{parts.join('.')}" | ||
| end | ||
|
|
||
| sig { params(req_string: String).returns(T::Array[String]) } | ||
| def convert_caret_req(req_string) | ||
| version = req_string.gsub(/^\^[\s=]*/, "") | ||
| parts = version.split(".") | ||
|
casey-robertson-paypal marked this conversation as resolved.
|
||
| parts.fill("x", parts.length...3) | ||
| first_non_zero = parts.find { |d| d != "0" } | ||
| first_non_zero_index = | ||
| first_non_zero ? T.must(parts.index(first_non_zero)) : parts.count - 1 | ||
| # If the requirement has a blank minor or patch version increment the | ||
| # previous index value with 1 | ||
| first_non_zero_index -= 1 if first_non_zero == "x" | ||
| upper_bound = parts.map.with_index do |part, i| | ||
| if i < first_non_zero_index then part | ||
| elsif i == first_non_zero_index then (part.to_i + 1).to_s | ||
| elsif i > first_non_zero_index && i == 2 then "0.a" | ||
| else "0" | ||
| end | ||
| end.join(".") | ||
|
|
||
| [">= #{version}", "< #{upper_bound}"] | ||
| end | ||
| end | ||
| end | ||
| end | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.