Skip to content

helm: support versioning-strategy (range-preserving updates)#15218

Open
casey-robertson-paypal wants to merge 24 commits into
dependabot:mainfrom
casey-robertson-paypal:casey/helm-versioning-strategy
Open

helm: support versioning-strategy (range-preserving updates)#15218
casey-robertson-paypal wants to merge 24 commits into
dependabot:mainfrom
casey-robertson-paypal:casey/helm-versioning-strategy

Conversation

@casey-robertson-paypal

@casey-robertson-paypal casey-robertson-paypal commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Implements #15216 — wires versioning-strategy into the helm ecosystem so Chart.yaml constraints can be preserved instead of always exact-pinned.

Heads-up on default behavior (open question below): exact-pinned constraints behave exactly as today (1.0.01.5.0). For caret/range constraints, this PR currently implements the documented increase semantics (^1.0.0^1.0.5, operator-preserving) rather than helm's historical exact-pin (^1.0.01.0.5). That's a behavior change for caret/range users who haven't set versioning-strategy — see the "Open question" comment; happy to make it fully opt-in instead.

Behavior

constraint latest increase increase-if-necessary widen
^1.0.0 1.0.5 (in range) ^1.0.5 no PR no PR
^1.0.0 2.0.0 (out) ^2.0.0 ^2.0.0 ^2.0.0
~1.2.0 1.2.9 (in range) ~1.2.9 no PR no PR
>=1.0.0 <2.0.0 2.5.0 (out) >=1.0.0 <3.0.0 >=1.0.0 <3.0.0 >=1.0.0 <3.0.0
1.0.0 (exact) 1.5.0 1.5.0 1.5.0 1.5.0

How it works

  • Helm::Requirement — a SemVer-range parser (helm uses Masterminds-style ^/~/ranges/||, unlike the gem-style Docker::Requirement registered today). Modeled on NpmAndYarn::Requirement; handles space- and comma-separated AND.
  • RequirementsUpdater — mirrors npm's, scoped to the three strategies.
  • UpdateChecker / ChartUpdater wiring — in-range updates are suppressed (no PR) under range-preserving strategies; multi-comparator ranges are anchored on their lowest named version (so they don't crash the version check) and rewritten requirements are quoted to keep Chart.yaml valid YAML.

A few design choices I'd genuinely welcome input on

  • Default/increase semantics — see the open-question comment below (legacy exact-pin vs. documented operator-preserving increase).
  • Helm::Requirement is used explicitly in the new code paths rather than re-registered globally, to avoid disturbing the docker-image path / ignore conditions. Happy to switch to a single registered class if preferred.
  • widen mirrors npm's exact semantics (caret/tilde bumped in place; </hyphen bounds widened; OR-append only for un-widenable ranges), not Renovate's || style — chosen for consistency with other Dependabot ecosystems.
  • Masterminds vs node-semver: covered the high-value gaps (comma-AND, range anchoring); a Go/Masterminds fidelity helper is sketched as a follow-up rather than built speculatively.

Testing

Unit specs for each piece (parser, updater, checker, chart updater) covering caret / tilde / exact / explicit-range / comma / OR across all three strategies.

Plus an end-to-end verification repo: casey-robertson-paypal/dependabot-helm runs this branch via dry-run against real OCI charts for all five constraint styles — committed transcripts + green CI + a results table in the run summary.

Honest caveat: that's this branch run via dry-run, not hosted Dependabot — it demonstrates the behavior, it isn't a production run.

Companion docs PR: github/docs#44584 (adds helm to the versioning-strategy reference). A helm smoke-tests companion is also planned once the default-behavior question below is settled.


Putting this out as a functional, tested starting point — I'm not attached to any specific choice here and would love feedback or a different direction. Just excited to help close the gap.

Adapts NpmAndYarn::Requirement to parse Helm Chart.yaml constraints
(caret, tilde, hyphen, x-range, OR, space- and comma-separated AND).
parse always returns a Helm::Version so satisfaction comparisons stay
Helm::Version vs Helm::Version (its <=> sig rejects plain Gem::Version).

Not registered globally; used explicitly by the range-preserving
requirements updater. No behavior change (no caller yet).
Mirrors npm's RequirementsUpdater for increase (BumpVersions),
increase-if-necessary (BumpVersionsIfNecessary), and widen (WidenRanges),
scoped to Helm and built on Helm::Requirement/Helm::Version. Widen follows
npm semantics (caret bump, <-bound widened in place), not Renovate's
OR-append. Not wired into the live update checker yet; no behavior change.
Route the Chart.yaml constraint (stored in dependency.version for helm
chart deps) through RequirementsUpdater so increase-if-necessary and widen
are honored, suppress in-range updates under range-preserving strategies
(no PR when the resolved version already satisfies the constraint), and
write the updated requirement string into Chart.yaml. The default
(increase / BumpVersions) reproduces the previous exact-pin behavior, so
existing users see no change unless they opt into a non-default strategy.
Anchor candidate filtering on the lowest version named in the constraint so
explicit ranges (>=1.0.0 <2.0.0, comma, ||) no longer crash version_class.new,
and quote rewritten requirements that need it so Chart.yaml stays valid YAML.
Adds strategy tests for tilde, explicit range, comma-AND, and OR.
Copilot AI review requested due to automatic review settings June 4, 2026 04:46
@casey-robertson-paypal casey-robertson-paypal requested a review from a team as a code owner June 4, 2026 04:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR adds range-preserving update behavior for Helm Chart.yaml dependencies by introducing Helm-specific requirement parsing and a requirements updater that honors Dependabot’s versioning strategies.

Changes:

  • Add Dependabot::Helm::Requirement to parse Masterminds-style SemVer ranges (caret, tilde, comparator ranges, OR, etc.).
  • Add Dependabot::Helm::UpdateChecker::RequirementsUpdater and wire it into the Helm update checker for :helm_chart dependencies.
  • Update the Helm chart file updater to write updated requirement strings (and quote them when needed for valid YAML), plus add fixtures and specs covering range behavior.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
helm/spec/fixtures/helm/charts/chart_with_range_dependency.yaml Adds fixture for caret-range dependencies in Chart.yaml.
helm/spec/fixtures/helm/charts/chart_with_explicit_range.yaml Adds fixture for explicit comparator-range dependencies that require quoting.
helm/spec/dependabot/helm/update_checker_spec.rb Extends update checker specs to cover range-preserving strategies and comparator ranges.
helm/spec/dependabot/helm/update_checker/requirements_updater_spec.rb Adds focused unit tests for requirements rewriting under multiple strategies and range syntaxes.
helm/spec/dependabot/helm/requirement_spec.rb Adds tests for Helm requirement parsing/satisfaction across supported range types.
helm/spec/dependabot/helm/file_updater/chart_updater_spec.rb Ensures Chart.yaml output uses updated requirement strings and quotes comparator ranges to keep YAML valid.
helm/lib/dependabot/helm/update_checker/requirements_updater.rb Introduces Helm-specific requirements updater (mirroring npm-like semantics).
helm/lib/dependabot/helm/update_checker.rb Integrates requirements updater, adds range-aware can_update? behavior and safer current-version anchoring.
helm/lib/dependabot/helm/requirement.rb Implements Helm/Masterminds-style requirement parsing and conversion to Ruby constraints.
helm/lib/dependabot/helm/file_updater/chart_updater.rb Writes updated requirement strings back to Chart.yaml and quotes unsafe YAML scalars.

Comment on lines +56 to +73
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
end

sig { params(requirements: T.nilable(T.any(String, T::Array[String]))).void }
def initialize(*requirements)
requirements = requirements.flatten
.flat_map { |req_string| T.must(req_string).split(",").map(&:strip) }
.flat_map { |req_string| convert_helm_constraint_to_ruby_constraint(req_string) }

super
end

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in be6cf17. new(nil) (from requirements_array(nil)) now resolves to a >= 0 match-anything constraint backed by a Helm::Version, instead of raising on T.must(nil). Added a spec covering the nil case.

Comment on lines +99 to +105
if current_requirement.match?(/(<|-\s)/i)
ruby_req = ruby_requirements(current_requirement).first
return req if ruby_req&.satisfied_by?(latest_resolvable_version)

updated_req = update_range_requirement(current_requirement)
return req.merge(requirement: updated_req)
end

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in be6cf17. The range branch now checks any? across all OR alternatives instead of only .first, so a later alternative that already permits the latest version is honored. Added a spec covering <1.0.0 || >=2.0.0.

Comment on lines +92 to +95
named = dependency.version.to_s.scan(/\d+(?:\.\d+)+/).filter_map do |v|
version_class.new(v) if version_class.correct?(v)
end
named.min || version_class.new("0")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Adjusted in be6cf17. The anchor scan is now \d+(?:\.\d+)*, so major-only tokens (e.g. >=1 <2) anchor on 1 rather than falling back to 0. (This fallback only runs for multi-comparator ranges that can't parse as a single version; for those the anchor just sets the floor for candidate filtering, so the resolved latest is unaffected — but matching major-only tokens is more correct.)

Comment thread helm/lib/dependabot/helm/update_checker/requirements_updater.rb Fixed
Comment thread helm/lib/dependabot/helm/update_checker/requirements_updater.rb Fixed
Comment thread helm/lib/dependabot/helm/update_checker/requirements_updater.rb Fixed
Comment thread helm/lib/dependabot/helm/update_checker/requirements_updater.rb Fixed
Comment thread helm/lib/dependabot/helm/update_checker/requirements_updater.rb Fixed
…or regex)

- Requirement#initialize: nil requirement now means match-anything (>= 0)
  instead of raising on T.must(nil).
- RequirementsUpdater: check all OR alternatives, not just the first, when
  deciding whether the latest version is already permitted.
- UpdateChecker#current_version: anchor regex also matches major-only tokens.
- Relax parse / latest_resolvable_version sigs to Dependabot::Version (the
  statically-inferred type of version_class.new).
- T.must the RequirementsUpdater result (.first is nilable).
- T.cast scan results to String before correct?.
- Resolve nilable Helm::Version#to_s in update_version_string.
@casey-robertson-paypal

casey-robertson-paypal commented Jun 4, 2026

Copy link
Copy Markdown
Contributor Author

Re: the red Smoke check — it's missing helm smoke wiring, not this PR's code.

smoke-filters.yml maps helm/** → the helm filter, but .github/smoke-matrix.json has no helm entry and dependabot/smoke-tests has no smoke-helm*.yaml, so the suite list comes back empty → the test job gets an empty matrix → the workflow errors. It's pre-existing: any PR touching helm/** trips this today.

I'll wire it up — a helm entry in .github/smoke-matrix.json here, plus a companion smoke-helm.yaml in dependabot/smoke-tests. I'm holding the recorded test until the default-behavior decision (in the comment above) is settled, since the recorded expectations depend on it — recording it first would just be rework. Flagging now so the red Smoke check isn't read as a regression.

Use possessive quantifiers in VERSION_REGEX, SEPARATOR, and the range
matcher so they stay linear on pathological input (resolves the CodeQL
polynomial-regex alerts). Matching is identical to the greedy forms for
real version constraints; full spec suite unchanged.
@casey-robertson-paypal

casey-robertson-paypal commented Jun 4, 2026

Copy link
Copy Markdown
Contributor Author

Open decision: default behavior for caret/range constraints

There's a conflict the issue's "increase (current behavior, default)" framing glosses over:

  • Helm today exact-pins everything: ^1.0.01.0.5 (the caret is stripped).
  • The documented increase strategy keeps the range and bumps the floor: ^1.0.0^1.0.5.

These coincide for exact pins (1.0.01.5.0) but diverge for caret/range constraints. As written, this PR implements the documented (operator-preserving) increase, so for caret/range users who haven't set versioning-strategy the default would change from exact-pin to operator-preserving. (I've corrected the PR description, which previously over-claimed the default was unchanged.)

Prior art — all of it points toward preserving ranges:

  • Helm's own chart best-practices: "Where possible, use version ranges instead of pinning to an exact version. The suggested default is to use a patch-level version match" (~1.2.3). Helm explicitly discourages exact-pinning.
  • Renovate defaults Helm to replace (operator-preserving — ^1.0.0^2.0.0, never collapsing to an exact pin); pinning is a separate, explicit opt-in.
  • Every other Dependabot ecosystem defaults to operator-preserving (bump_versions / widen); none exact-pin. Helm's exact-pin is the outlier in the repo.

So operator-preserving reads as the correct, ecosystem-consistent default, and helm's exact-pin looks like a pre-versioning-strategy wart that fights both Helm's guidance and the author's declared intent.

That said, it's a backward-compat decision for your users, so I'd rather you make the call:

  1. Adopt the operator-preserving default (consistent with the above), accepting it changes behavior for existing unset caret/range users — or keep helm's exact-pin as the unset default and make range-preservation purely opt-in?
  2. What does the service pass to core for helm when versioning-strategy is unset (nil, or a resolved strategy)? That bounds what core can do for the default.

Happy to implement either shape. Path forward: point me at a default, I'll finalize the code if it needs adjusting, then land the helm smoke-tests companion (its recorded expectations depend on this decision). The docs PR (github/docs#44584) is already open.

casey-robertson-paypal and others added 13 commits June 3, 2026 23:14
BumpVersions preserves the authored operator (^1.0.0 -> ^1.0.5); it does
not reproduce helm's prior exact-pin behavior. See the default-behavior
discussion on the PR.
Resolve conflict in helm/update_checker.rb: combine main's DependencyRequirement
return type + wrap_requirements helper with the versioning-strategy routing
(helm_chart deps through RequirementsUpdater, image deps exact-overwrite).
Matches the existing .rubocop_todo.yml treatment of every other ecosystem's
requirements_updater (all use T::Hash[Symbol, T.untyped] for the requirement
hash, all grandfathered under this cop).
@casey-robertson-paypal

casey-robertson-paypal commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

Hey @thavaahariharangit! Hoping I could get your eyes on this when you have some spare cycles (or a pointer to whoever owns helm these days). It's been open about two weeks, CI and CodeQL are green, and it's caught up with main.

There's one thing I'd really value a maintainer's read on before I finalize. Should helm's default (when no versioning-strategy is set) keep today's exact-pin behavior, or move to the documented operator-preserving increase? I dug into the prior art (Helm's own docs, Renovate, and every other Dependabot ecosystem) and they all lean toward preserving ranges, but changing the default for existing users felt like your call, not mine. I wrote it up in this comment.

No rush at all, and totally fine if you want to hand it off. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants