Skip to content

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

Merged
robaiken merged 70 commits into
dependabot:mainfrom
casey-robertson-paypal:casey/helm-versioning-strategy
Jul 10, 2026
Merged

helm: support versioning-strategy (range-preserving updates)#15218
robaiken merged 70 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.

🔖 Open decisions for reviewers

Status: All review feedback has been triaged and addressed. Correctness has been verified two ways: a full unit suite, and a differential test that runs 735 constraint×version pairs through both this parser and the real Go Masterminds/semver library helm uses — 0 divergences on every common form (caret incl. 0.x, tilde/~>, comparators, !=, comma/space-AND, hyphen incl. partial, OR, wildcards, comparator+wildcard, v-prefix, +digest). A holistic review pass also fixed several structural bugs (file-corruption and a crash in multi-occurrence handling, segment/empty-constraint crashes) and simplified the constraint anchor to use Requirement#min_version. CI is green. Three decisions remain — all genuinely a maintainer's call, not oversights:

  1. Default increase semantics — for caret/range users who have not set versioning-strategy, this PR currently applies the documented operator-preserving behavior (^1.0.0^1.0.5) instead of helm's historical exact-pin (^1.0.01.0.5). Exact pins are unaffected (1.0.01.5.0). Prior art favors operator-preserving (Helm's own chart best-practices, Renovate, and every other Dependabot ecosystem), but changing the default for un-opted users is a backward-compat decision I don't want to make unilaterally — happy to make it fully opt-in instead. → discussion

  2. Full Masterminds fidelity for exotic constraint forms — the common forms match Masterminds exactly (the 0/735 differential above). A handful of rare forms still follow RubyGems rather than Masterminds semantics, because Helm::Requirement converts to Gem::Requirement: prereleases admitted within ordinary ranges (^1.0.0 vs 1.1.0-rc1), =/!= x-ranges (=1.x, !=1.x), partial comparators (>1.2 starts at 1.3.0), and ~0.0.0. These are uncommon in real Chart.yamls and closing them fully touches core satisfaction, so I've proposed them as a focused follow-up rather than expanding this PR — but happy to land them here if you'd prefer.

  3. Scope: keep or drop multi-occurrence handling? When the same chart name is depended on from multiple files/entries with different constraints, DependencySet merges them into one dependency with several requirements, and this PR updates each occurrence by its own source[:tag]. It works and is fully tested now, but honestly it's been the source of most of the review churn — the per-occurrence machinery is combinatorial and generated several edge-case bugs (file-corruption on aliasing entries, an "unchanged file" crash, and an anchor that gated on the wrong occurrence — all fixed + regression-tested). It supports a case that's rare in real charts. Option A: keep it as-is (hardened). Option B: drop it to a focused follow-up and handle only the common one-requirement-per-chart case here, shrinking the PR. I lean slightly toward B for reviewability, but it's a scope call I'd rather you make.

Everything else raised in review is resolved.

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, +build/digest suffixes, hyphen ranges, and x-ranges.
  • RequirementsUpdater — mirrors npm's, scoped to the three strategies.
  • UpdateChecker / ChartUpdater wiring — in-range updates are suppressed (no PR) under range-preserving strategies; version_can_update? is gated on whether the authored constraint would actually change (so it never hands the file updater an unchanged Chart.yaml); multi-comparator ranges are anchored on their lowest lower-bound version; each requirement is updated by its own source[:tag] (a chart depended on from multiple files keeps its per-occurrence constraint); 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 decision (1) above.
  • 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, digest suffixes); prerelease gating — decision (2) above — and a Go/Masterminds fidelity helper are sketched as follow-ups rather than built speculatively.

Testing

Unit specs for each piece (parser, updater, checker, chart updater) covering caret / tilde / exact / explicit-range / hyphen / comma / OR / wildcard / build-metadata across all three strategies, plus a round-trip invariant guard (any Helm::Version-valid pin parses and self-satisfies).

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 above 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 thread helm/lib/dependabot/helm/requirement.rb
Comment thread helm/lib/dependabot/helm/update_checker/requirements_updater.rb
Comment thread helm/lib/dependabot/helm/update_checker.rb Outdated
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).

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

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

Comment thread helm/lib/dependabot/helm/requirement.rb
Comment thread helm/lib/dependabot/helm/requirement.rb
Comment thread helm/lib/dependabot/helm/requirement.rb
… public consumer

Follow-up review fixes:
- min_bound_anchor: an OR constraint with a branch that has no lower bound
  ("<=2.0.0 || >=10.0.0") permits arbitrarily low versions, so anchor at 0
  rather than the other branch's floor.
- Drop the direct current_version private-method assertions; verify the anchor
  through filter_valid_releases (the public-facing consumer the spec already
  covers), matching the file's existing convention.
Copilot AI review requested due to automatic review settings July 10, 2026 06:14

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

Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.

Comment thread helm/lib/dependabot/helm/update_checker/requirements_updater.rb Outdated
Comment thread helm/lib/dependabot/helm/update_checker.rb
update_range_requirement replaced the first textual occurrence of the upper
bound, which could hit an earlier lower-bound operand sharing the same version
string. Anchor the substitution to the < / <= / hyphen so only the upper bound
moves.
Copilot AI review requested due to automatic review settings July 10, 2026 06:33

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

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

Comment thread helm/lib/dependabot/helm/requirement.rb
Comment thread helm/lib/dependabot/helm/update_checker/requirements_updater.rb
Comment thread helm/lib/dependabot/helm/update_checker.rb Outdated
Follow-up to the OR-anchor fix: min_version reports nil for an exact (=)
requirement, so "1.0.0 || 2.0.0" was treated as unbounded-below and anchored
at 0. An exact branch's floor is its pinned version; only genuinely
lower-unbounded branches (< / <= / !=) fall back to 0.
Copilot AI review requested due to automatic review settings July 10, 2026 06: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

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Comment thread helm/lib/dependabot/helm/file_updater/chart_updater.rb
The multi-file skip-unchanged guard also masked genuine rewrite failures: an
entry the regex can't locate (e.g. a quoted name, or version listed before
name) produced unchanged content that the file updater then skipped, emitting a
partial PR with no error. Now the writer skips only entries whose constraint is
genuinely unchanged, tolerates a quoted name, and raises when a *changed*
constraint has no matching entry.
Copilot AI review requested due to automatic review settings July 10, 2026 07:07

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

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Comment thread helm/lib/dependabot/helm/update_checker.rb Outdated
current_version anchored on the single combined dependency.version, so when a
chart is merged from several occurrences with different constraints, a higher-
floored occurrence (e.g. ^5.0.0) could gate out a release that a lower one
(^1.0.0) still needs updated to. Anchor on the lowest occurrence constraint
instead, consistent with the per-occurrence handling elsewhere.
Copilot AI review requested due to automatic review settings July 10, 2026 07:17

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

Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.

Comment on lines +63 to +67
# This occurrence's constraint is unchanged (the strategy left it
# alone) — nothing to write for it.
next if new_requirement == old_version

new_content, cursor = replace_next_entry_version(
end
def replace_next_entry_version(content, cursor, old_version, new_version)
pattern = /
(\s+-\s+name:\s+["']?#{Regexp.escape(dependency.name)}["']?.*?\n\s+version:\s+)
Copilot AI review requested due to automatic review settings July 10, 2026 13:49

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.

Review details

  • Files reviewed: 14/14 changed files
  • Comments generated: 2
  • Review effort level: Medium

Comment on lines +69 to +80
def updated_chart_requirement(req)
current_constraint = chart_constraint_for(req)
synthetic = T.cast(req.merge(requirement: current_constraint), Dependabot::DependencyRequirement)

T.must(
RequirementsUpdater.new(
requirements: [synthetic],
update_strategy: resolved_update_strategy,
latest_resolvable_version: T.must(latest_version).to_s
).updated_requirements.first
)
end
end
def replace_next_entry_version(content, cursor, old_version, new_version)
pattern = /
(\s+-\s+name:\s+["']?#{Regexp.escape(dependency.name)}["']?.*?\n\s+version:\s+)
@robaiken robaiken merged commit e347107 into dependabot:main Jul 10, 2026
59 checks passed
@robaiken

Copy link
Copy Markdown
Contributor

@casey-robertson-paypal Thanks for the contribution, this change has now been deployed! Really appreciate you adding the version strategy for Helm

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.

4 participants