dependabot-helm 0.385.0 → 0.386.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/dependabot/helm/file_parser.rb +8 -7
- data/lib/dependabot/helm/file_updater/chart_updater.rb +81 -21
- data/lib/dependabot/helm/file_updater.rb +5 -0
- data/lib/dependabot/helm/requirement.rb +218 -0
- data/lib/dependabot/helm/update_checker/latest_version_resolver.rb +2 -2
- data/lib/dependabot/helm/update_checker/requirements_updater.rb +273 -0
- data/lib/dependabot/helm/update_checker.rb +165 -18
- metadata +8 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4d830487e84adfa0aa822028c60564e4052db9bf39d261bf776df1b4214d62bf
|
|
4
|
+
data.tar.gz: c8677544088532a69e90f056281c28578f279e826ddc624f98b80709563212c3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bf871f1010bf02f2e455046ea4e076e8a433c47f11a043ff95e82975d5ff19584b9a8cc8a99eeb1f64c71489665b6da669d37e19c747bbebf4c162d3e7802fff
|
|
7
|
+
data.tar.gz: 06206be3e63e348226371f69a1f1ede06c14402f3648867dd9749d4bedc749a6b75e63a230ad14d15e47c0d8eb3a61c933c658165620f6c61a6316c5eb191cb5
|
|
@@ -39,13 +39,13 @@ module Dependabot
|
|
|
39
39
|
|
|
40
40
|
sig do
|
|
41
41
|
params(
|
|
42
|
-
yaml: T::Hash[
|
|
42
|
+
yaml: T::Hash[String, Object],
|
|
43
43
|
chart_file: Dependabot::DependencyFile,
|
|
44
44
|
dependency_set: DependencySet
|
|
45
45
|
).void
|
|
46
46
|
end
|
|
47
47
|
def parse_dependencies(yaml, chart_file, dependency_set)
|
|
48
|
-
yaml["dependencies"].each do |dep|
|
|
48
|
+
T.cast(yaml["dependencies"], T::Array[Object]).each do |dep|
|
|
49
49
|
next unless dep.is_a?(Hash) && dep["name"] && dep["version"]
|
|
50
50
|
|
|
51
51
|
parsed_line = {
|
|
@@ -95,7 +95,7 @@ module Dependabot
|
|
|
95
95
|
next unless yaml.is_a?(Hash)
|
|
96
96
|
|
|
97
97
|
find_images_in_hash(yaml).each do |image_details|
|
|
98
|
-
parsed_line = extract_image_details(image_details[:image])
|
|
98
|
+
parsed_line = extract_image_details(T.must(image_details[:image]))
|
|
99
99
|
next unless parsed_line
|
|
100
100
|
|
|
101
101
|
version = version_from(parsed_line)
|
|
@@ -132,7 +132,7 @@ module Dependabot
|
|
|
132
132
|
params(
|
|
133
133
|
key: String,
|
|
134
134
|
value: String,
|
|
135
|
-
hash: T::Hash[
|
|
135
|
+
hash: T::Hash[String, Object],
|
|
136
136
|
current_path: T::Array[String]
|
|
137
137
|
).returns(T::Array[T::Hash[Symbol, String]])
|
|
138
138
|
end
|
|
@@ -153,7 +153,7 @@ module Dependabot
|
|
|
153
153
|
end
|
|
154
154
|
|
|
155
155
|
sig do
|
|
156
|
-
params(value: T::Array[
|
|
156
|
+
params(value: T::Array[Object], current_path: T::Array[String]).returns(T::Array[T::Hash[Symbol, String]])
|
|
157
157
|
end
|
|
158
158
|
def handle_array_value(value, current_path)
|
|
159
159
|
images = []
|
|
@@ -163,9 +163,10 @@ module Dependabot
|
|
|
163
163
|
images
|
|
164
164
|
end
|
|
165
165
|
|
|
166
|
-
sig { params(hash:
|
|
166
|
+
sig { params(hash: Object, path: T::Array[String]).returns(T::Array[T::Hash[Symbol, String]]) }
|
|
167
167
|
def find_images_in_hash(hash, path = [])
|
|
168
|
-
images = []
|
|
168
|
+
images = T.let([], T::Array[T::Hash[Symbol, String]])
|
|
169
|
+
return images unless hash.is_a?(Hash)
|
|
169
170
|
|
|
170
171
|
hash.each do |key, value|
|
|
171
172
|
current_path = path + [key.to_s]
|
|
@@ -23,16 +23,16 @@ module Dependabot
|
|
|
23
23
|
@dependency = dependency
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
# Returns the file content with this dependency's entries rewritten. May
|
|
27
|
+
# return the content unchanged (e.g. a strategy that leaves an in-range
|
|
28
|
+
# constraint alone, or a dependency not present in this file); the file
|
|
29
|
+
# updater decides whether a file actually changed.
|
|
26
30
|
sig { params(file: Dependabot::DependencyFile).returns(T.nilable(String)) }
|
|
27
31
|
def updated_chart_yaml_content(file)
|
|
28
32
|
content = file.content
|
|
29
33
|
yaml_obj = YAML.safe_load(T.must(content))
|
|
30
34
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
raise "Expected content to change!" if content == file.content
|
|
34
|
-
|
|
35
|
-
content
|
|
35
|
+
update_chart_dependencies(T.must(content), yaml_obj, file)
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
private
|
|
@@ -43,31 +43,91 @@ module Dependabot
|
|
|
43
43
|
sig do
|
|
44
44
|
params(
|
|
45
45
|
content: String,
|
|
46
|
-
yaml_obj: T::Hash[
|
|
46
|
+
yaml_obj: T::Hash[String, Object],
|
|
47
47
|
file: Dependabot::DependencyFile
|
|
48
48
|
).returns(String)
|
|
49
49
|
end
|
|
50
50
|
def update_chart_dependencies(content, yaml_obj, file)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
51
|
+
return content unless update_chart_dependency?(file) && yaml_obj["dependencies"]
|
|
52
|
+
|
|
53
|
+
# Rewrite each entry once, scanning forward so repeated occurrences of
|
|
54
|
+
# the same chart update independently. A whole-file gsub per entry can
|
|
55
|
+
# alias — after entry A is rewritten to entry B's old version, B's pass
|
|
56
|
+
# would re-match the just-updated A.
|
|
57
|
+
cursor = 0
|
|
58
|
+
T.cast(yaml_obj["dependencies"], T::Array[T::Hash[String, Object]]).each do |dep|
|
|
59
|
+
next unless dep["name"] == dependency.name
|
|
60
|
+
|
|
61
|
+
old_version = dep["version"].to_s
|
|
62
|
+
new_requirement = updated_requirement_string(file, old_version) || dependency.version.to_s
|
|
63
|
+
# This occurrence's constraint is unchanged (the strategy left it
|
|
64
|
+
# alone) — nothing to write for it.
|
|
65
|
+
next if new_requirement == old_version
|
|
66
|
+
|
|
67
|
+
new_content, cursor = replace_next_entry_version(
|
|
68
|
+
content,
|
|
69
|
+
cursor,
|
|
70
|
+
old_version,
|
|
71
|
+
yaml_safe_value(new_requirement)
|
|
72
|
+
)
|
|
73
|
+
# A changed constraint that produced no textual edit means the entry
|
|
74
|
+
# wasn't matched (e.g. an unusual name/version layout). Surface it
|
|
75
|
+
# rather than silently emitting a partial update.
|
|
76
|
+
if new_content == content
|
|
77
|
+
raise "Expected to update #{dependency.name} from #{old_version} to " \
|
|
78
|
+
"#{new_requirement} in #{file.name}, but no matching entry was found"
|
|
66
79
|
end
|
|
80
|
+
|
|
81
|
+
content = new_content
|
|
67
82
|
end
|
|
68
83
|
content
|
|
69
84
|
end
|
|
70
85
|
|
|
86
|
+
# Replaces this chart's next `version:` occurrence (at/after cursor) with
|
|
87
|
+
# new_version, returning the updated content and the position just past
|
|
88
|
+
# the rewrite so later entries match their own line. The name may be
|
|
89
|
+
# quoted; returns the content unchanged when no entry matches.
|
|
90
|
+
sig do
|
|
91
|
+
params(content: String, cursor: Integer, old_version: String, new_version: String)
|
|
92
|
+
.returns([String, Integer])
|
|
93
|
+
end
|
|
94
|
+
def replace_next_entry_version(content, cursor, old_version, new_version)
|
|
95
|
+
pattern = /
|
|
96
|
+
(\s+-\s+name:\s+["']?#{Regexp.escape(dependency.name)}["']?.*?\n\s+version:\s+)
|
|
97
|
+
["']?#{Regexp.escape(old_version)}["']?
|
|
98
|
+
/mx
|
|
99
|
+
match = pattern.match(content, cursor)
|
|
100
|
+
return [content, cursor] unless match
|
|
101
|
+
|
|
102
|
+
rewritten = "#{match[1]}#{new_version}"
|
|
103
|
+
updated = T.must(content[0...match.begin(0)]) + rewritten + T.must(content[match.end(0)..])
|
|
104
|
+
[updated, match.begin(0) + rewritten.length]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Wrap a requirement in double quotes when it would otherwise be
|
|
108
|
+
# ambiguous as a YAML plain scalar: any whitespace, or a leading
|
|
109
|
+
# YAML-indicator character (">", "<", "~", "|", etc.). Simple values
|
|
110
|
+
# (exact, caret) are left bare, preserving the previous output.
|
|
111
|
+
sig { params(value: String).returns(String) }
|
|
112
|
+
def yaml_safe_value(value)
|
|
113
|
+
return "\"#{value}\"" if value.match?(/\s/) || value.match?(/\A[>~<|&*!%@?:#,\[\]{}]/)
|
|
114
|
+
|
|
115
|
+
value
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# The strategy-updated requirement string for a specific chart entry,
|
|
119
|
+
# matched by the entry's authored version (source[:tag]) so repeated
|
|
120
|
+
# occurrences of the same chart name each get their own update. Falls
|
|
121
|
+
# back to the first requirement for the file, then to nil (exact pin).
|
|
122
|
+
sig { params(file: Dependabot::DependencyFile, old_version: String).returns(T.nilable(String)) }
|
|
123
|
+
def updated_requirement_string(file, old_version)
|
|
124
|
+
reqs = dependency.requirements.select do |r|
|
|
125
|
+
r[:file] == file.name && r.dig(:metadata, :type) == :helm_chart
|
|
126
|
+
end
|
|
127
|
+
req = reqs.find { |r| r.dig(:source, :tag) == old_version } || reqs.first
|
|
128
|
+
req && req[:requirement]
|
|
129
|
+
end
|
|
130
|
+
|
|
71
131
|
sig { params(file: Dependabot::DependencyFile).returns(T::Boolean) }
|
|
72
132
|
def update_chart_dependency?(file)
|
|
73
133
|
reqs = dependency.requirements.select { |r| r[:file] == file.name }
|
|
@@ -47,6 +47,11 @@ module Dependabot
|
|
|
47
47
|
|
|
48
48
|
if file.name.match?(CHART_YAML_REGEXP)
|
|
49
49
|
updated_content = chart_updater.updated_chart_yaml_content(file)
|
|
50
|
+
# A dependency may span several files/entries; when a strategy leaves
|
|
51
|
+
# this file's constraint unchanged (e.g. an already-in-range range),
|
|
52
|
+
# skip it rather than emit an identical file (which would raise).
|
|
53
|
+
next if updated_content == file.content
|
|
54
|
+
|
|
50
55
|
updated_files << updated_file(
|
|
51
56
|
file: file,
|
|
52
57
|
content: T.must(updated_content)
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "sorbet-runtime"
|
|
5
|
+
|
|
6
|
+
require "dependabot/requirement"
|
|
7
|
+
require "dependabot/utils"
|
|
8
|
+
require "dependabot/helm/version"
|
|
9
|
+
|
|
10
|
+
module Dependabot
|
|
11
|
+
module Helm
|
|
12
|
+
# Parses Helm Chart.yaml SemVer range constraints (^, ~, x-ranges, hyphen
|
|
13
|
+
# ranges, || OR, space-AND and comma-AND). Adapted from
|
|
14
|
+
# Dependabot::NpmAndYarn::Requirement: Helm uses the same SemVer family
|
|
15
|
+
# (Masterminds), unlike the Gem-style Docker::Requirement registered for the
|
|
16
|
+
# docker-image path.
|
|
17
|
+
#
|
|
18
|
+
# NOTE: this class is intentionally NOT registered as the requirement class
|
|
19
|
+
# for "helm" (that remains Docker::Requirement, used by the docker-image
|
|
20
|
+
# path and ignore conditions). It is used explicitly by the range-preserving
|
|
21
|
+
# requirements updater.
|
|
22
|
+
class Requirement < Dependabot::Requirement
|
|
23
|
+
extend T::Sig
|
|
24
|
+
|
|
25
|
+
AND_SEPARATOR = T.let(/(?<=[a-zA-Z0-9*])\s+(?:&+\s+)?(?!\s*[|-])/, Regexp)
|
|
26
|
+
OR_SEPARATOR = T.let(/(?<=[a-zA-Z0-9*])\s*\|+/, Regexp)
|
|
27
|
+
|
|
28
|
+
# Allow an optional 'v' prefix and an optional '+<build metadata/digest>'
|
|
29
|
+
# suffix. Helm::Version supports the latter (e.g. "1.0.119807+<digest>"
|
|
30
|
+
# from OCI charts), so the requirement parser must accept it too or
|
|
31
|
+
# constraints containing it raise BadRequirementError.
|
|
32
|
+
quoted = OPS.keys.map { |k| Regexp.quote(k) }.join("|")
|
|
33
|
+
version_pattern = "v?#{Gem::Version::VERSION_PATTERN}(?:\\+[0-9A-Za-z\\-.]+)?"
|
|
34
|
+
|
|
35
|
+
PATTERN_RAW = T.let("\\s*(#{quoted})?\\s*(#{version_pattern})\\s*".freeze, String)
|
|
36
|
+
PATTERN = T.let(/\A#{PATTERN_RAW}\z/, Regexp)
|
|
37
|
+
|
|
38
|
+
# Always returns a Helm::Version (never the plain Gem::Version-backed
|
|
39
|
+
# DefaultRequirement), so satisfaction comparisons stay Helm::Version vs
|
|
40
|
+
# Helm::Version — Helm::Version#<=> has a strict sig that rejects plain
|
|
41
|
+
# Gem::Version operands.
|
|
42
|
+
sig do
|
|
43
|
+
params(obj: T.any(String, Gem::Version))
|
|
44
|
+
.returns(T::Array[T.any(String, Dependabot::Version)])
|
|
45
|
+
end
|
|
46
|
+
def self.parse(obj)
|
|
47
|
+
return ["=", Helm::Version.new(obj.to_s)] if obj.is_a?(Gem::Version)
|
|
48
|
+
|
|
49
|
+
unless (matches = PATTERN.match(obj.to_s))
|
|
50
|
+
raise BadRequirementError, "Illformed requirement [#{obj.inspect}]"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
[matches[1] || "=", Helm::Version.new(T.must(matches[2]))]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Returns an array of requirements. At least one requirement from the
|
|
57
|
+
# returned array must be satisfied for a version to be valid.
|
|
58
|
+
sig { override.params(requirement_string: T.nilable(String)).returns(T::Array[Requirement]) }
|
|
59
|
+
def self.requirements_array(requirement_string)
|
|
60
|
+
return [new(nil)] if requirement_string.nil?
|
|
61
|
+
|
|
62
|
+
# Parentheses are extremely rare in Helm constraints; strip them.
|
|
63
|
+
requirement_string = requirement_string.gsub(/[()]/, "")
|
|
64
|
+
requirement_string.strip.split(OR_SEPARATOR).map do |req_string|
|
|
65
|
+
new(req_string.strip.split(AND_SEPARATOR))
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
sig { params(requirements: T.nilable(T.any(String, T::Array[String]))).void }
|
|
70
|
+
def initialize(*requirements)
|
|
71
|
+
requirements = requirements.flatten.compact
|
|
72
|
+
# `new(nil)` (from `requirements_array(nil)`) means "match anything".
|
|
73
|
+
# Use a literal ">= 0" so it flows through our `parse` and is backed by a
|
|
74
|
+
# Helm::Version — Gem's DefaultRequirement uses a plain Gem::Version, which
|
|
75
|
+
# Helm::Version#<=>'s strict sig would reject.
|
|
76
|
+
requirements = [">= 0"] if requirements.empty?
|
|
77
|
+
|
|
78
|
+
requirements = requirements.flat_map { |req_string| req_string.split(",").map(&:strip) }
|
|
79
|
+
.flat_map { |req_string| convert_helm_constraint_to_ruby_constraint(req_string) }
|
|
80
|
+
|
|
81
|
+
super
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
sig { params(req_string: String).returns(T.any(String, T::Array[String])) }
|
|
87
|
+
def convert_helm_constraint_to_ruby_constraint(req_string)
|
|
88
|
+
# Leave dist-tags / non-numeric leading tokens untouched.
|
|
89
|
+
return req_string if req_string.match?(/^([A-Za-uw-z]|v[^\d])/)
|
|
90
|
+
|
|
91
|
+
# Wildcards combined with an operator need dedicated handling before the
|
|
92
|
+
# generic wildcard strip below (which would corrupt them).
|
|
93
|
+
wildcard = wildcard_ruby_constraint(req_string)
|
|
94
|
+
return wildcard if wildcard
|
|
95
|
+
|
|
96
|
+
dispatch_ruby_constraint(req_string.gsub(/(?:\.|^)[xX*]/, ""))
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Dispatches a wildcard-stripped constraint token to its converter.
|
|
100
|
+
sig { params(req_string: String).returns(T.any(String, T::Array[String])) }
|
|
101
|
+
def dispatch_ruby_constraint(req_string)
|
|
102
|
+
if req_string.empty? then ">= 0"
|
|
103
|
+
elsif req_string.start_with?("~>") then req_string
|
|
104
|
+
elsif req_string.start_with?("=") then req_string.gsub(/^=*/, "")
|
|
105
|
+
elsif req_string.start_with?("~") then convert_tilde_req(req_string)
|
|
106
|
+
elsif req_string.start_with?("^") then convert_caret_req(req_string)
|
|
107
|
+
elsif req_string.include?(" - ") then convert_hyphen_req(req_string)
|
|
108
|
+
elsif req_string.match?(/[<>]/) then req_string
|
|
109
|
+
else ruby_range(req_string)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Handles wildcard constraints that a leading operator would otherwise
|
|
114
|
+
# corrupt. Returns nil when there is no operator+wildcard to expand.
|
|
115
|
+
# - "<=1.x"/">1.2.x": expand from the wildcard's [floor, ceiling) span
|
|
116
|
+
# (stripping the wildcard first would turn "<=1.x" into "<=1", which
|
|
117
|
+
# wrongly rejects 1.5.0 that Masterminds accepts).
|
|
118
|
+
# - "*"/"^*"/"~*": a bare wildcard means "any version"; any other
|
|
119
|
+
# conversion yields an empty bound.
|
|
120
|
+
sig { params(req_string: String).returns(T.nilable(String)) }
|
|
121
|
+
def wildcard_ruby_constraint(req_string)
|
|
122
|
+
if (m = req_string.match(/\A(<=|>=|<|>)\s*(\d+(?:\.\d+)*)\.[xX*]\z/))
|
|
123
|
+
return convert_wildcard_comparator(T.must(m[1]), T.must(m[2]))
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
return ">= 0" if req_string.match?(/\A[\^~<>=\s]*[xX*]+\z/)
|
|
127
|
+
|
|
128
|
+
nil
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Expands a comparator applied to a wildcard version ("1.x" spans
|
|
132
|
+
# [1.0.0, 2.0.0); "1.2.x" spans [1.2.0, 1.3.0)) per Masterminds semantics:
|
|
133
|
+
# >= X.x -> >= floor > X.x -> >= ceiling
|
|
134
|
+
# < X.x -> < floor <= X.x -> < ceiling
|
|
135
|
+
sig { params(operator: String, prefix: String).returns(String) }
|
|
136
|
+
def convert_wildcard_comparator(operator, prefix)
|
|
137
|
+
parts = prefix.split(".").map(&:to_i)
|
|
138
|
+
floor = (parts + [0, 0, 0]).first(3)
|
|
139
|
+
ceiling_parts = parts.dup
|
|
140
|
+
ceiling_parts[-1] = T.must(ceiling_parts[-1]) + 1
|
|
141
|
+
ceiling = (ceiling_parts + [0, 0, 0]).first(3)
|
|
142
|
+
|
|
143
|
+
case operator
|
|
144
|
+
when ">=" then ">= #{floor.join('.')}"
|
|
145
|
+
when ">" then ">= #{ceiling.join('.')}"
|
|
146
|
+
when "<" then "< #{floor.join('.')}"
|
|
147
|
+
else "< #{ceiling.join('.')}" # "<="
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
sig { params(req_string: String).returns(String) }
|
|
152
|
+
def convert_tilde_req(req_string)
|
|
153
|
+
version = req_string.gsub(/^~\>?[\s=]*/, "")
|
|
154
|
+
parts = version.split(".")
|
|
155
|
+
parts << "0" if parts.count < 3
|
|
156
|
+
"~> #{parts.join('.')}"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
sig { params(req_string: String).returns(T::Array[String]) }
|
|
160
|
+
def convert_hyphen_req(req_string)
|
|
161
|
+
parts = req_string.split(/\s+-\s+/)
|
|
162
|
+
lower_bound = T.must(parts[0])
|
|
163
|
+
upper_bound = T.must(parts[1])
|
|
164
|
+
lower_bound_parts = lower_bound.split(".")
|
|
165
|
+
lower_bound_parts.fill("0", lower_bound_parts.length...3)
|
|
166
|
+
|
|
167
|
+
upper_bound_parts = upper_bound.split(".")
|
|
168
|
+
upper_bound_range =
|
|
169
|
+
if upper_bound_parts.length < 3
|
|
170
|
+
# When upper bound is a partial version treat these as an X-range:
|
|
171
|
+
# "1.0 - 2.0" includes the whole 2.0.x series, so always increment
|
|
172
|
+
# the last specified component (even when it is 0) before padding —
|
|
173
|
+
# otherwise the bound becomes "< 2.0.0.a" and excludes 2.0.0.
|
|
174
|
+
upper_bound_parts[-1] = (T.must(upper_bound_parts[-1]).to_i + 1).to_s
|
|
175
|
+
upper_bound_parts.fill("0", upper_bound_parts.length...3)
|
|
176
|
+
"< #{upper_bound_parts.join('.')}.a"
|
|
177
|
+
else
|
|
178
|
+
"<= #{upper_bound_parts.join('.')}"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
[">= #{lower_bound_parts.join('.')}", upper_bound_range]
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
sig { params(req_string: String).returns(String) }
|
|
185
|
+
def ruby_range(req_string)
|
|
186
|
+
parts = req_string.split(".")
|
|
187
|
+
# If we have three or more parts then this is an exact match
|
|
188
|
+
return req_string if parts.count >= 3
|
|
189
|
+
|
|
190
|
+
# If we have fewer than three parts we do a partial match
|
|
191
|
+
parts << "0"
|
|
192
|
+
"~> #{parts.join('.')}"
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
sig { params(req_string: String).returns(T::Array[String]) }
|
|
196
|
+
def convert_caret_req(req_string)
|
|
197
|
+
version = req_string.gsub(/^\^[\s=]*/, "")
|
|
198
|
+
parts = version.split(".")
|
|
199
|
+
parts.fill("x", parts.length...3)
|
|
200
|
+
first_non_zero = parts.find { |d| d != "0" }
|
|
201
|
+
first_non_zero_index =
|
|
202
|
+
first_non_zero ? T.must(parts.index(first_non_zero)) : parts.count - 1
|
|
203
|
+
# If the requirement has a blank minor or patch version increment the
|
|
204
|
+
# previous index value with 1
|
|
205
|
+
first_non_zero_index -= 1 if first_non_zero == "x"
|
|
206
|
+
upper_bound = parts.map.with_index do |part, i|
|
|
207
|
+
if i < first_non_zero_index then part
|
|
208
|
+
elsif i == first_non_zero_index then (part.to_i + 1).to_s
|
|
209
|
+
elsif i > first_non_zero_index && i == 2 then "0.a"
|
|
210
|
+
else "0"
|
|
211
|
+
end
|
|
212
|
+
end.join(".")
|
|
213
|
+
|
|
214
|
+
[">= #{version}", "< #{upper_bound}"]
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
@@ -49,11 +49,11 @@ module Dependabot
|
|
|
49
49
|
|
|
50
50
|
sig do
|
|
51
51
|
params(
|
|
52
|
-
versions: T::Array[T::Hash[String,
|
|
52
|
+
versions: T::Array[T::Hash[String, Object]],
|
|
53
53
|
repo_name: T.nilable(String),
|
|
54
54
|
chart_name: T.nilable(String)
|
|
55
55
|
)
|
|
56
|
-
.returns(T::Array[T::Hash[String,
|
|
56
|
+
.returns(T::Array[T::Hash[String, Object]])
|
|
57
57
|
end
|
|
58
58
|
def fetch_tag_and_release_date_helm_chart(versions, repo_name, chart_name)
|
|
59
59
|
Dependabot.logger.info("Filtering versions in cooldown period from chart: #{repo_name}")
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
################################################################################
|
|
5
|
+
# Helm Chart.yaml dependency constraints use SemVer ranges (Masterminds), the #
|
|
6
|
+
# same family npm uses. This mirrors #
|
|
7
|
+
# Dependabot::NpmAndYarn::UpdateChecker::RequirementsUpdater, scoped to the #
|
|
8
|
+
# three strategies Helm supports, with the git-source / JSR branches removed. #
|
|
9
|
+
################################################################################
|
|
10
|
+
|
|
11
|
+
require "sorbet-runtime"
|
|
12
|
+
|
|
13
|
+
require "dependabot/dependency_requirement"
|
|
14
|
+
require "dependabot/helm/requirement"
|
|
15
|
+
require "dependabot/helm/update_checker"
|
|
16
|
+
require "dependabot/helm/version"
|
|
17
|
+
require "dependabot/requirements_update_strategy"
|
|
18
|
+
|
|
19
|
+
module Dependabot
|
|
20
|
+
module Helm
|
|
21
|
+
class UpdateChecker
|
|
22
|
+
class RequirementsUpdater
|
|
23
|
+
extend T::Sig
|
|
24
|
+
|
|
25
|
+
# Possessive quantifiers (++) keep these linear on pathological input
|
|
26
|
+
# (no catastrophic/polynomial backtracking) while matching identically
|
|
27
|
+
# to the greedy forms for real version constraints. The trailing group
|
|
28
|
+
# consumes an optional '+<build metadata/digest>' suffix so rewrites
|
|
29
|
+
# replace it wholesale instead of leaving a stale suffix behind
|
|
30
|
+
# (e.g. "1.2.3+old" -> "1.5.0", not "1.5.0+old").
|
|
31
|
+
VERSION_REGEX = /[0-9]++(?:\.[A-Za-z0-9\-_]++)*+(?:\+[A-Za-z0-9\-_.]++)?/
|
|
32
|
+
SEPARATOR = /(?<=[a-zA-Z0-9*])[\s|]++(?![\s|-])/
|
|
33
|
+
# The version attached to an upper comparator (`<`/`<=`) or a hyphen
|
|
34
|
+
# endpoint — the bound that widening moves.
|
|
35
|
+
UPPER_COMPARATOR_REGEX = /<=?\s*(#{VERSION_REGEX})/
|
|
36
|
+
HYPHEN_UPPER_REGEX = /\s-\s+(#{VERSION_REGEX})/
|
|
37
|
+
ALLOWED_UPDATE_STRATEGIES = T.let(
|
|
38
|
+
[
|
|
39
|
+
RequirementsUpdateStrategy::LockfileOnly,
|
|
40
|
+
RequirementsUpdateStrategy::WidenRanges,
|
|
41
|
+
RequirementsUpdateStrategy::BumpVersions,
|
|
42
|
+
RequirementsUpdateStrategy::BumpVersionsIfNecessary
|
|
43
|
+
].freeze,
|
|
44
|
+
T::Array[Dependabot::RequirementsUpdateStrategy]
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
sig do
|
|
48
|
+
params(
|
|
49
|
+
requirements: T::Array[Dependabot::DependencyRequirement],
|
|
50
|
+
update_strategy: Dependabot::RequirementsUpdateStrategy,
|
|
51
|
+
latest_resolvable_version: T.nilable(T.any(String, Gem::Version))
|
|
52
|
+
).void
|
|
53
|
+
end
|
|
54
|
+
def initialize(requirements:, update_strategy:, latest_resolvable_version:)
|
|
55
|
+
@requirements = requirements
|
|
56
|
+
@update_strategy = update_strategy
|
|
57
|
+
|
|
58
|
+
check_update_strategy
|
|
59
|
+
|
|
60
|
+
return unless latest_resolvable_version
|
|
61
|
+
|
|
62
|
+
@latest_resolvable_version = T.let(
|
|
63
|
+
version_class.new(latest_resolvable_version),
|
|
64
|
+
Dependabot::Version
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
sig { returns(T::Array[Dependabot::DependencyRequirement]) }
|
|
69
|
+
def updated_requirements
|
|
70
|
+
return requirements if update_strategy.lockfile_only?
|
|
71
|
+
|
|
72
|
+
requirements.map do |req|
|
|
73
|
+
next req unless latest_resolvable_version
|
|
74
|
+
next req unless req[:requirement]
|
|
75
|
+
# Leave dist-tags / non-numeric leading tokens untouched.
|
|
76
|
+
next req if req[:requirement].match?(/^([A-Za-uw-z]|v[^\d])/)
|
|
77
|
+
|
|
78
|
+
case update_strategy
|
|
79
|
+
when RequirementsUpdateStrategy::WidenRanges then widen_requirement(req)
|
|
80
|
+
when RequirementsUpdateStrategy::BumpVersions then update_version_requirement(req)
|
|
81
|
+
when RequirementsUpdateStrategy::BumpVersionsIfNecessary
|
|
82
|
+
update_version_requirement_if_needed(req)
|
|
83
|
+
else raise "Unexpected update strategy: #{update_strategy}"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
sig { returns(T::Array[Dependabot::DependencyRequirement]) }
|
|
91
|
+
attr_reader :requirements
|
|
92
|
+
|
|
93
|
+
sig { returns(Dependabot::RequirementsUpdateStrategy) }
|
|
94
|
+
attr_reader :update_strategy
|
|
95
|
+
|
|
96
|
+
sig { returns(T.nilable(Dependabot::Version)) }
|
|
97
|
+
attr_reader :latest_resolvable_version
|
|
98
|
+
|
|
99
|
+
sig { void }
|
|
100
|
+
def check_update_strategy
|
|
101
|
+
return if ALLOWED_UPDATE_STRATEGIES.include?(update_strategy)
|
|
102
|
+
|
|
103
|
+
raise "Unknown update strategy: #{update_strategy}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
sig { params(req: Dependabot::DependencyRequirement).returns(Dependabot::DependencyRequirement) }
|
|
107
|
+
def update_version_requirement(req)
|
|
108
|
+
current_requirement = req[:requirement]
|
|
109
|
+
return req if current_requirement.strip == ""
|
|
110
|
+
|
|
111
|
+
if current_requirement.match?(/(<|-\s)/i)
|
|
112
|
+
# Check every OR alternative, not just the first — a later branch may
|
|
113
|
+
# already permit the latest version.
|
|
114
|
+
return req if ruby_requirements(current_requirement).any? { |r| r.satisfied_by?(latest_resolvable_version) }
|
|
115
|
+
|
|
116
|
+
updated_req = update_range_requirement(current_requirement)
|
|
117
|
+
return T.cast(req.merge(requirement: updated_req), Dependabot::DependencyRequirement)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Strict lower-bound (`>`) and not-equal (`!=`) constraints are already
|
|
121
|
+
# permitted by the resolved version, so leave them untouched. Rewriting
|
|
122
|
+
# the numeric part would exclude the very version we want
|
|
123
|
+
# (`>1.0.0` -> `>2.0.0`, `!=1.0.0` -> `!=2.0.0`) and drop any sibling
|
|
124
|
+
# comparators.
|
|
125
|
+
if current_requirement.match?(/!=|(?<![<>=])>(?!=)/) &&
|
|
126
|
+
ruby_requirements(current_requirement).any? { |r| r.satisfied_by?(latest_resolvable_version) }
|
|
127
|
+
return req
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
reqs = current_requirement.strip.split(SEPARATOR).map(&:strip)
|
|
131
|
+
T.cast(req.merge(requirement: update_version_string(reqs.first)), Dependabot::DependencyRequirement)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Blank, or already permitted by the resolved version, under any strategy.
|
|
135
|
+
sig { params(req: Dependabot::DependencyRequirement).returns(T::Boolean) }
|
|
136
|
+
def already_satisfied?(req)
|
|
137
|
+
current_requirement = req[:requirement]
|
|
138
|
+
return true if current_requirement.strip == ""
|
|
139
|
+
|
|
140
|
+
ruby_requirements(current_requirement).any? { |r| r.satisfied_by?(latest_resolvable_version) }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
sig { params(req: Dependabot::DependencyRequirement).returns(Dependabot::DependencyRequirement) }
|
|
144
|
+
def update_version_requirement_if_needed(req)
|
|
145
|
+
return req if already_satisfied?(req)
|
|
146
|
+
|
|
147
|
+
update_version_requirement(req)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
sig { params(req: Dependabot::DependencyRequirement).returns(Dependabot::DependencyRequirement) }
|
|
151
|
+
def widen_requirement(req)
|
|
152
|
+
return req if already_satisfied?(req)
|
|
153
|
+
|
|
154
|
+
current_requirement = req[:requirement]
|
|
155
|
+
reqs = current_requirement.strip.split(SEPARATOR).map(&:strip)
|
|
156
|
+
|
|
157
|
+
updated_requirement =
|
|
158
|
+
if reqs.any? { |r| r.match?(/(<|-\s)/i) }
|
|
159
|
+
update_range_requirement(current_requirement)
|
|
160
|
+
elsif reqs.one?
|
|
161
|
+
update_version_string(current_requirement)
|
|
162
|
+
else
|
|
163
|
+
# An OR of caret/tilde/exact alternatives, none of which permit the
|
|
164
|
+
# latest version: widen by adding a new alternative rather than
|
|
165
|
+
# rewriting the authored ones (npm's widen_ranges behavior).
|
|
166
|
+
"#{current_requirement} || ^#{latest_resolvable_version}"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
T.cast(req.merge(requirement: updated_requirement), Dependabot::DependencyRequirement)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
sig { params(requirement_string: String).returns(T::Array[Helm::Requirement]) }
|
|
173
|
+
def ruby_requirements(requirement_string)
|
|
174
|
+
Helm::Requirement.requirements_array(requirement_string)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
sig { params(req_string: String).returns(String) }
|
|
178
|
+
def update_range_requirement(req_string)
|
|
179
|
+
range_requirements =
|
|
180
|
+
req_string.split(SEPARATOR).select { |r| r.match?(/<|(\s++-\s++)/) }
|
|
181
|
+
|
|
182
|
+
if range_requirements.one?
|
|
183
|
+
range_requirement = T.must(range_requirements.first)
|
|
184
|
+
versions = range_requirement.scan(VERSION_REGEX)
|
|
185
|
+
# The bound to move is the operand of `<`/`<=` (or the hyphen
|
|
186
|
+
# endpoint), NOT the numerically-largest token — a comma-AND `!=`
|
|
187
|
+
# operand can be larger (e.g. "!=9.0.0,<2.0.0"). Substitute the
|
|
188
|
+
# *original* matched text, not the parsed version's normalized
|
|
189
|
+
# #to_s: Helm::Version normalizes "2.0.0-rc1" to "2.0.0", so subbing
|
|
190
|
+
# the normalized form would match the wrong span and leave a stale
|
|
191
|
+
# suffix ("<3.0.0-rc1"). Fall back to the max token only if neither
|
|
192
|
+
# an upper comparator nor a hyphen endpoint is present.
|
|
193
|
+
original_upper = T.cast(
|
|
194
|
+
range_requirement[UPPER_COMPARATOR_REGEX, 1] ||
|
|
195
|
+
range_requirement[HYPHEN_UPPER_REGEX, 1] ||
|
|
196
|
+
T.must(versions.max_by { |v| version_class.new(v.to_s) }),
|
|
197
|
+
String
|
|
198
|
+
)
|
|
199
|
+
new_upper_bound = update_greatest_version(
|
|
200
|
+
version_class.new(original_upper).to_s,
|
|
201
|
+
T.must(latest_resolvable_version)
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Anchor the substitution to the upper-bound operator (`<`/`<=`) or
|
|
205
|
+
# hyphen so it can't replace an earlier lower-bound operand that
|
|
206
|
+
# happens to share the same version string.
|
|
207
|
+
req_string.sub(/(<=?\s*|\s-\s+)#{Regexp.escape(original_upper)}/) do
|
|
208
|
+
"#{Regexp.last_match(1)}#{new_upper_bound}"
|
|
209
|
+
end
|
|
210
|
+
else
|
|
211
|
+
req_string + " || ^#{T.must(latest_resolvable_version)}"
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
sig { params(req_string: String).returns(String) }
|
|
216
|
+
def update_version_string(req_string)
|
|
217
|
+
latest = T.must(latest_resolvable_version).to_s
|
|
218
|
+
req_string
|
|
219
|
+
.sub(VERSION_REGEX) do |old_version|
|
|
220
|
+
if old_version.match?(/\d-/) || latest.match?(/\d-/)
|
|
221
|
+
latest
|
|
222
|
+
else
|
|
223
|
+
old_parts = old_version.split(".")
|
|
224
|
+
new_parts = latest.split(".")
|
|
225
|
+
# Iterate the authored segments so the constraint's shape is
|
|
226
|
+
# preserved even when the latest version has fewer segments
|
|
227
|
+
# (e.g. "1.2.x" + latest "4.5" -> "4.5.x", not "4.5").
|
|
228
|
+
old_parts.map.with_index do |old, i|
|
|
229
|
+
# Preserve an authored wildcard segment, keeping its case
|
|
230
|
+
# ("1.x" -> "4.x", "1.X" -> "4.X").
|
|
231
|
+
old.match?(/\A[xX]/) ? old : (new_parts[i] || "0")
|
|
232
|
+
end.join(".")
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
sig { params(old_version: String, version_to_be_permitted: Dependabot::Version).returns(String) }
|
|
238
|
+
def update_greatest_version(old_version, version_to_be_permitted)
|
|
239
|
+
version = version_class.new(old_version)
|
|
240
|
+
version = version.release if version.prerelease?
|
|
241
|
+
|
|
242
|
+
index_to_update =
|
|
243
|
+
version.segments.map.with_index { |seg, i| T.cast(seg, Integer).zero? ? 0 : i }.max || 0
|
|
244
|
+
|
|
245
|
+
version.segments.map.with_index do |_, index|
|
|
246
|
+
segment_value =
|
|
247
|
+
if index < index_to_update
|
|
248
|
+
permitted_segment(version_to_be_permitted, index)
|
|
249
|
+
elsif index == index_to_update
|
|
250
|
+
permitted_segment(version_to_be_permitted, index) + 1
|
|
251
|
+
else
|
|
252
|
+
0
|
|
253
|
+
end
|
|
254
|
+
segment_value.to_s
|
|
255
|
+
end.join(".")
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# A version segment as an Integer, treating missing trailing segments as
|
|
259
|
+
# 0 (the permitted version may have fewer segments than the old bound).
|
|
260
|
+
sig { params(version: Dependabot::Version, index: Integer).returns(Integer) }
|
|
261
|
+
def permitted_segment(version, index)
|
|
262
|
+
seg = version.segments[index]
|
|
263
|
+
seg.nil? ? 0 : T.cast(seg, Integer)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
sig { returns(T.class_of(Helm::Version)) }
|
|
267
|
+
def version_class
|
|
268
|
+
Helm::Version
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
@@ -5,7 +5,9 @@ require "sorbet-runtime"
|
|
|
5
5
|
require "dependabot/update_checkers"
|
|
6
6
|
require "dependabot/update_checkers/base"
|
|
7
7
|
require "dependabot/errors"
|
|
8
|
+
require "dependabot/requirements_update_strategy"
|
|
8
9
|
require "dependabot/helm/version"
|
|
10
|
+
require "dependabot/helm/requirement"
|
|
9
11
|
require "dependabot/docker/requirement"
|
|
10
12
|
require "dependabot/shared/utils/credentials_finder"
|
|
11
13
|
require "dependabot/shared_helpers"
|
|
@@ -16,10 +18,15 @@ require "dependabot/helm/helpers"
|
|
|
16
18
|
|
|
17
19
|
module Dependabot
|
|
18
20
|
module Helm
|
|
19
|
-
|
|
21
|
+
# ClassLength is disabled to match sibling ecosystems' update checkers
|
|
22
|
+
# (npm_and_yarn and bun disable it on the same class). The version-fetching
|
|
23
|
+
# helpers already delegate to update_checker/latest_version_resolver;
|
|
24
|
+
# further extraction would fragment the update-decision flow.
|
|
25
|
+
class UpdateChecker < Dependabot::UpdateCheckers::Base # rubocop:disable Metrics/ClassLength
|
|
20
26
|
extend T::Sig
|
|
21
27
|
|
|
22
28
|
require_relative "update_checker/latest_version_resolver"
|
|
29
|
+
require_relative "update_checker/requirements_updater"
|
|
23
30
|
|
|
24
31
|
sig { override.returns(T.nilable(T.any(String, Gem::Version))) }
|
|
25
32
|
def latest_version
|
|
@@ -41,17 +48,116 @@ module Dependabot
|
|
|
41
48
|
return dependency.requirements unless latest_version
|
|
42
49
|
|
|
43
50
|
updated_reqs = dependency.requirements.map do |req|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
case req.dig(:metadata, :type)
|
|
52
|
+
when nil then req
|
|
53
|
+
when :helm_chart then updated_chart_requirement(req)
|
|
54
|
+
else req.merge(requirement: latest_version.to_s) # image deps: exact overwrite
|
|
55
|
+
end
|
|
49
56
|
end
|
|
50
57
|
wrap_requirements(updated_reqs)
|
|
51
58
|
end
|
|
52
59
|
|
|
53
60
|
private
|
|
54
61
|
|
|
62
|
+
# Helm stores each occurrence's constraint in the requirement's
|
|
63
|
+
# source[:tag], not the requirement field (the shared parser leaves it
|
|
64
|
+
# nil). Feed that constraint through the RequirementsUpdater so
|
|
65
|
+
# versioning-strategy is honored. When no strategy is set we default to
|
|
66
|
+
# BumpVersions, which preserves the authored operator and bumps the floor
|
|
67
|
+
# (e.g. `^1.0.0` -> `^1.0.5`); exact pins stay exact (`1.0.0` -> `1.5.0`).
|
|
68
|
+
sig { params(req: Dependabot::DependencyRequirement).returns(Dependabot::DependencyRequirement) }
|
|
69
|
+
def updated_chart_requirement(req)
|
|
70
|
+
current_constraint = chart_constraint_for(req)
|
|
71
|
+
synthetic = T.cast(req.merge(requirement: current_constraint), Dependabot::DependencyRequirement)
|
|
72
|
+
|
|
73
|
+
T.must(
|
|
74
|
+
RequirementsUpdater.new(
|
|
75
|
+
requirements: [synthetic],
|
|
76
|
+
update_strategy: resolved_update_strategy,
|
|
77
|
+
latest_resolvable_version: T.must(latest_version).to_s
|
|
78
|
+
).updated_requirements.first
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# The authored constraint for a single requirement. Prefer the
|
|
83
|
+
# requirement's own source[:tag] over dependency.version, since
|
|
84
|
+
# DependencySet may merge several same-named occurrences (different files
|
|
85
|
+
# or ranges) into one dependency with a single combined version.
|
|
86
|
+
sig { params(req: Dependabot::DependencyRequirement).returns(String) }
|
|
87
|
+
def chart_constraint_for(req)
|
|
88
|
+
(req[:requirement] || req.dig(:source, :tag) || dependency.version).to_s
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
sig { returns(Dependabot::RequirementsUpdateStrategy) }
|
|
92
|
+
def resolved_update_strategy
|
|
93
|
+
requirements_update_strategy || RequirementsUpdateStrategy::BumpVersions
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Overrides Base#current_version. For chart deps the Chart.yaml constraint
|
|
97
|
+
# lives in dependency.version, which may be a range with no single version;
|
|
98
|
+
# we anchor on its lower bound so candidate filtering has a concrete
|
|
99
|
+
# baseline. Non-chart deps (docker images) keep the base behavior.
|
|
100
|
+
sig { override.returns(T.nilable(Dependabot::Version)) }
|
|
101
|
+
def current_version
|
|
102
|
+
return super unless dependency_type == :helm_chart
|
|
103
|
+
|
|
104
|
+
@current_version ||= T.let(chart_anchor_version, T.nilable(Dependabot::Version))
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# A concrete version to anchor candidate filtering on. A dependency may
|
|
108
|
+
# merge several same-named occurrences with different constraints, so anchor
|
|
109
|
+
# on the *lowest* occurrence — otherwise a higher-floored occurrence would
|
|
110
|
+
# gate out a release that a lower one still needs updated to.
|
|
111
|
+
sig { returns(Dependabot::Version) }
|
|
112
|
+
def chart_anchor_version
|
|
113
|
+
constraints = dependency.requirements
|
|
114
|
+
.select { |r| r.dig(:metadata, :type) == :helm_chart }
|
|
115
|
+
.map { |r| chart_constraint_for(r) }
|
|
116
|
+
constraints = [dependency.version.to_s] if constraints.empty?
|
|
117
|
+
T.must(constraints.map { |c| anchor_for_constraint(c) }.min)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Anchors a single constraint on a concrete version. Single/lenient forms
|
|
121
|
+
# (^1.0.0, ~1.2.0, 1.0.0) parse directly; comparator/hyphen/OR ranges do
|
|
122
|
+
# not, so anchor on the lowest lower bound the Requirement parser reports
|
|
123
|
+
# (0 when the constraint has no lower bound, e.g. "<2.0.0").
|
|
124
|
+
sig { params(raw: String).returns(Dependabot::Version) }
|
|
125
|
+
def anchor_for_constraint(raw)
|
|
126
|
+
# A comparator/hyphen/OR constraint has no single version, and
|
|
127
|
+
# version_class.new would silently mis-read it ("<2.0.0" -> 2.0.0), so
|
|
128
|
+
# route those to the parser's lower bound.
|
|
129
|
+
return min_bound_anchor(raw) if raw.match?(/[<>]|!=|\s-\s|\|\|/)
|
|
130
|
+
|
|
131
|
+
begin
|
|
132
|
+
version_class.new(raw)
|
|
133
|
+
rescue StandardError
|
|
134
|
+
min_bound_anchor(raw)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# The lowest lower-bound version across the constraint's OR branches, or 0
|
|
139
|
+
# when any branch has no lower bound (that branch permits arbitrarily low
|
|
140
|
+
# versions, e.g. "<=2.0.0 || >=10.0.0"). Re-wrapped as a Helm::Version —
|
|
141
|
+
# min_version yields a base Dependabot::Version, but Helm::Version#<=> only
|
|
142
|
+
# accepts Helm operands.
|
|
143
|
+
sig { params(raw: String).returns(Dependabot::Version) }
|
|
144
|
+
def min_bound_anchor(raw)
|
|
145
|
+
floors = Helm::Requirement.requirements_array(raw).map { |req| branch_lower_bound(req) }
|
|
146
|
+
# A nil floor means some OR branch permits arbitrarily low versions
|
|
147
|
+
# (an upper-only or exclusion branch), so anchor at 0.
|
|
148
|
+
return version_class.new("0") if floors.any?(&:nil?)
|
|
149
|
+
|
|
150
|
+
floor = floors.min
|
|
151
|
+
floor ? version_class.new(floor.to_s) : version_class.new("0")
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# The lowest version an OR branch permits: its >=/>/~> lower bound, or an
|
|
155
|
+
# exact (=) pin's version. Nil when the branch has no lower bound (</<=/!=).
|
|
156
|
+
sig { params(req: Dependabot::Requirement).returns(T.nilable(Gem::Version)) }
|
|
157
|
+
def branch_lower_bound(req)
|
|
158
|
+
req.min_version || req.requirements.filter_map { |op, v| v if op == "=" }.min
|
|
159
|
+
end
|
|
160
|
+
|
|
55
161
|
sig { override.returns(T::Array[Dependabot::Dependency]) }
|
|
56
162
|
def updated_dependencies_after_full_unlock
|
|
57
163
|
raise NotImplementedError
|
|
@@ -77,11 +183,20 @@ module Dependabot
|
|
|
77
183
|
valid_releases = latest_version_resolver
|
|
78
184
|
.fetch_tag_and_release_date_helm_chart(valid_releases, repo_name, chart_name)
|
|
79
185
|
end
|
|
80
|
-
highest_release = valid_releases.max_by { |release| version_class.new(release["version"]) }
|
|
186
|
+
highest_release = valid_releases.max_by { |release| version_class.new(T.cast(release["version"], String)) }
|
|
81
187
|
Dependabot.logger.info(
|
|
82
188
|
"Found latest version #{T.must(highest_release)['version']} for #{chart_name} using helm search"
|
|
83
189
|
)
|
|
84
|
-
version_class.new(T.must(highest_release)["version"])
|
|
190
|
+
version_class.new(T.cast(T.must(highest_release)["version"], String))
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
sig do
|
|
194
|
+
params(index: T.nilable(T::Hash[String, Object]), chart_name: String)
|
|
195
|
+
.returns(T.nilable(T::Array[T::Hash[String, Object]]))
|
|
196
|
+
end
|
|
197
|
+
def chart_entries_from_index(index, chart_name)
|
|
198
|
+
entries = T.cast(index&.fetch("entries", nil), T.nilable(T::Hash[String, Object]))
|
|
199
|
+
T.cast(entries&.fetch(chart_name, nil), T.nilable(T::Array[T::Hash[String, Object]]))
|
|
85
200
|
end
|
|
86
201
|
|
|
87
202
|
sig { params(chart_name: String, repo_url: T.nilable(String)).returns(T.nilable(Gem::Version)) }
|
|
@@ -91,9 +206,10 @@ module Dependabot
|
|
|
91
206
|
|
|
92
207
|
index_url = build_index_url(repo_url)
|
|
93
208
|
index = fetch_helm_chart_index(index_url)
|
|
94
|
-
|
|
209
|
+
chart_entries = chart_entries_from_index(index, chart_name)
|
|
210
|
+
return nil unless chart_entries
|
|
95
211
|
|
|
96
|
-
all_versions =
|
|
212
|
+
all_versions = chart_entries.map { |entry| T.cast(entry["version"], String) }
|
|
97
213
|
Dependabot.logger.info("Found #{all_versions.length} versions for #{chart_name} in index.yaml")
|
|
98
214
|
|
|
99
215
|
valid_versions = filter_valid_versions(all_versions)
|
|
@@ -115,12 +231,16 @@ module Dependabot
|
|
|
115
231
|
highest_version
|
|
116
232
|
end
|
|
117
233
|
|
|
118
|
-
sig { params(releases: T::Array[T::Hash[String,
|
|
234
|
+
sig { params(releases: T::Array[T::Hash[String, Object]]).returns(T::Array[T::Hash[String, Object]]) }
|
|
119
235
|
def filter_valid_releases(releases)
|
|
120
236
|
releases.reject do |release|
|
|
121
|
-
version_class.new(release["version"]
|
|
237
|
+
release_version = version_class.new(T.cast(release["version"], String))
|
|
238
|
+
# Compare against current_version (the anchored floor) rather than
|
|
239
|
+
# dependency.version: for a range constraint (">=1.0.0 <2.0.0") the raw
|
|
240
|
+
# version string isn't a single parseable version.
|
|
241
|
+
release_version <= current_version ||
|
|
122
242
|
ignore_requirements.any? do |r|
|
|
123
|
-
r.instance_of?(Dependabot::Requirement) && r.satisfied_by?(
|
|
243
|
+
r.instance_of?(Dependabot::Requirement) && r.satisfied_by?(release_version)
|
|
124
244
|
end
|
|
125
245
|
end
|
|
126
246
|
end
|
|
@@ -139,10 +259,37 @@ module Dependabot
|
|
|
139
259
|
end
|
|
140
260
|
|
|
141
261
|
sig { params(requirements_to_unlock: T.nilable(Symbol)).returns(T::Boolean) }
|
|
142
|
-
def version_can_update?(requirements_to_unlock:)
|
|
262
|
+
def version_can_update?(requirements_to_unlock:)
|
|
143
263
|
return false unless latest_version
|
|
264
|
+
# Non-chart deps (docker images) keep the base behavior — including its
|
|
265
|
+
# nilable current_version handling.
|
|
266
|
+
return super unless dependency_type == :helm_chart
|
|
267
|
+
|
|
268
|
+
return false unless version_class.new(latest_version.to_s) > T.must(current_version)
|
|
269
|
+
|
|
270
|
+
# For a chart dependency, an "update" means the authored Chart.yaml
|
|
271
|
+
# constraint actually changes. The RequirementsUpdater deliberately
|
|
272
|
+
# leaves some constraints untouched (an in-range comparator/hyphen range,
|
|
273
|
+
# or an OR range already satisfied by the latest version), so gate on
|
|
274
|
+
# whether it would produce a different constraint. Otherwise can_update?
|
|
275
|
+
# promises a change the file updater can't make, and it raises
|
|
276
|
+
# "Expected content to change!" on the unchanged Chart.yaml.
|
|
277
|
+
chart_requirement_changes?
|
|
278
|
+
end
|
|
144
279
|
|
|
145
|
-
|
|
280
|
+
# Whether running the authored chart constraint through the
|
|
281
|
+
# RequirementsUpdater yields a different constraint string for *any*
|
|
282
|
+
# requirement. A dependency may carry several requirements (same chart
|
|
283
|
+
# name across files/entries); an update is warranted if even one changes.
|
|
284
|
+
sig { returns(T::Boolean) }
|
|
285
|
+
def chart_requirement_changes?
|
|
286
|
+
chart_reqs = dependency.requirements.select { |r| r.dig(:metadata, :type) == :helm_chart }
|
|
287
|
+
chart_reqs = dependency.requirements if chart_reqs.empty?
|
|
288
|
+
return true if chart_reqs.empty?
|
|
289
|
+
|
|
290
|
+
chart_reqs.any? do |req|
|
|
291
|
+
updated_chart_requirement(req)[:requirement].to_s != chart_constraint_for(req)
|
|
292
|
+
end
|
|
146
293
|
end
|
|
147
294
|
|
|
148
295
|
sig { returns(T.nilable(T.any(String, Gem::Version))) }
|
|
@@ -170,7 +317,7 @@ module Dependabot
|
|
|
170
317
|
chart_name: String,
|
|
171
318
|
repo_name: T.nilable(String),
|
|
172
319
|
repo_url: T.nilable(String)
|
|
173
|
-
).returns(T.nilable(T::Array[T::Hash[String,
|
|
320
|
+
).returns(T.nilable(T::Array[T::Hash[String, Object]]))
|
|
174
321
|
end
|
|
175
322
|
def fetch_chart_releases(chart_name, repo_name = nil, repo_url = nil)
|
|
176
323
|
Dependabot.logger.info("Fetching releases for Helm chart: #{chart_name}")
|
|
@@ -267,7 +414,7 @@ module Dependabot
|
|
|
267
414
|
name
|
|
268
415
|
end
|
|
269
416
|
|
|
270
|
-
sig { params(index_url: String).returns(T.nilable(T::Hash[
|
|
417
|
+
sig { params(index_url: String).returns(T.nilable(T::Hash[String, Object])) }
|
|
271
418
|
def fetch_helm_chart_index(index_url)
|
|
272
419
|
Dependabot.logger.info("Fetching Helm chart index from #{index_url}")
|
|
273
420
|
|
|
@@ -296,7 +443,7 @@ module Dependabot
|
|
|
296
443
|
sig { params(all_versions: T::Array[String]).returns(T::Array[String]) }
|
|
297
444
|
def filter_valid_versions(all_versions)
|
|
298
445
|
all_versions.reject do |version|
|
|
299
|
-
version_class.new(version) <=
|
|
446
|
+
version_class.new(version) <= current_version ||
|
|
300
447
|
ignore_requirements.any? do |r|
|
|
301
448
|
r.instance_of?(Dependabot::Requirement) && r.satisfied_by?(version_class.new(version))
|
|
302
449
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dependabot-helm
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.386.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dependabot
|
|
@@ -15,28 +15,28 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - '='
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.
|
|
18
|
+
version: 0.386.0
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - '='
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.
|
|
25
|
+
version: 0.386.0
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: dependabot-docker
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
30
|
- - '='
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: 0.
|
|
32
|
+
version: 0.386.0
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - '='
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: 0.
|
|
39
|
+
version: 0.386.0
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
41
|
name: debug
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -265,15 +265,17 @@ files:
|
|
|
265
265
|
- lib/dependabot/helm/helpers.rb
|
|
266
266
|
- lib/dependabot/helm/package/package_details_fetcher.rb
|
|
267
267
|
- lib/dependabot/helm/package_manager.rb
|
|
268
|
+
- lib/dependabot/helm/requirement.rb
|
|
268
269
|
- lib/dependabot/helm/update_checker.rb
|
|
269
270
|
- lib/dependabot/helm/update_checker/latest_version_resolver.rb
|
|
271
|
+
- lib/dependabot/helm/update_checker/requirements_updater.rb
|
|
270
272
|
- lib/dependabot/helm/version.rb
|
|
271
273
|
homepage: https://github.com/dependabot/dependabot-core
|
|
272
274
|
licenses:
|
|
273
275
|
- MIT
|
|
274
276
|
metadata:
|
|
275
277
|
bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
|
|
276
|
-
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.
|
|
278
|
+
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.386.0
|
|
277
279
|
rdoc_options: []
|
|
278
280
|
require_paths:
|
|
279
281
|
- lib
|