dependabot-helm 0.302.0 → 0.303.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6aff5fba7351aca9138cda923c9f15b308c4cc05185fba589fda95eb09560ae1
4
- data.tar.gz: ae1d94b09275d5cfd7764b6465c39d9b93b446fdf80ee5822dea1032522eec2e
3
+ metadata.gz: 53be03f1fcf2d32d85e040547378b00c860458ae23f782cba4fd437de7defa44
4
+ data.tar.gz: 3d55823e45e4fa2e31b80603e0613f1929b0f2512cd34bc39a49a88c8a8aa647
5
5
  SHA512:
6
- metadata.gz: 69f08273fb1022d70271b457b7212115881f6320fd1fc2cc05f5bfb34c15c19a54581656a133ed1c44570a2af4f5045a66b87abf5ce5118ddde8dcf316e4a0f1
7
- data.tar.gz: e0ccd801ddc647007f2868f5d6d834320ec7e01da5d7f6bddca40ef766628fe589865982d530cb31ede447a663370de40213b57b4c5c64a99efbd9ed4e8e4ec4
6
+ metadata.gz: fd62305c3145cf79ee84cc653d50a8ad1cf0f77855c80c4dce6932e92c20835447d6b99c63453bc3fb1fa23ecdad0960ab363ac414cba6a4e2a159e5fc0c8888
7
+ data.tar.gz: 57c31fc74e516b7eeade03b18959bc902f546ccd39b22263a6dc0bf7b5c76e63a0d414be88bf41364d850f07ef5f76bea01a475561cd2dcd16584f4a2514edd8
@@ -7,11 +7,15 @@ module Dependabot
7
7
  module Helm
8
8
  class FileFetcher < Dependabot::Shared::SharedFileFetcher
9
9
  FILENAME_REGEX = /.*\.ya?ml$/i
10
+ CHART_LOCK_REGEXP = /Chart\.lock/i
10
11
 
11
12
  sig { override.returns(T::Array[DependencyFile]) }
12
13
  def fetch_files
14
+ return [] unless allow_beta_ecosystems?
15
+
13
16
  fetched_files = []
14
- fetched_files += correctly_encoded_helm_files if allow_beta_ecosystems?
17
+ fetched_files += correctly_encoded_helm_files
18
+ fetched_files += chart_locks
15
19
 
16
20
  return fetched_files if fetched_files.any?
17
21
 
@@ -31,6 +35,14 @@ module Dependabot
31
35
  .map { |f| fetch_file_from_host(f.name) }, T.nilable(T::Array[DependencyFile]))
32
36
  end
33
37
 
38
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
39
+ def chart_locks
40
+ @chart_locks ||=
41
+ T.let(repo_contents(raise_errors: false)
42
+ .select { |f| f.type == "file" && f.name.match?(CHART_LOCK_REGEXP) }
43
+ .map { |f| fetch_file_from_host(f.name) }, T.nilable(T::Array[DependencyFile]))
44
+ end
45
+
34
46
  sig { returns(T::Array[Dependabot::DependencyFile]) }
35
47
  def correctly_encoded_helm_files
36
48
  helm_files.select { |f| T.must(f.content).valid_encoding? }
@@ -11,7 +11,9 @@ module Dependabot
11
11
  extend T::Sig
12
12
 
13
13
  CHART_YAML = /.*chart\.ya?ml$/i
14
+ CHART_LOCK = /.*chart\.lock$/i
14
15
  VALUES_YAML = /.*values\.ya?ml$/i
16
+ DEFAULT_REPOSITORY = "https://charts.helm.sh/stable"
15
17
 
16
18
  sig { returns(Ecosystem) }
17
19
  def ecosystem
@@ -41,26 +43,38 @@ module Dependabot
41
43
  end
42
44
  def parse_dependencies(yaml, chart_file, dependency_set)
43
45
  yaml["dependencies"].each do |dep|
44
- next unless dep.is_a?(Hash) && dep["name"] && dep["version"] && dep["repository"]
46
+ next unless dep.is_a?(Hash) && dep["name"] && dep["version"]
45
47
 
46
48
  parsed_line = {
47
49
  "image" => dep["name"],
48
50
  "tag" => dep["version"],
49
- "registry" => dep["repository"],
51
+ "registry" => repository_from_registry(dep["repository"]),
50
52
  "digest" => nil
51
53
  }
52
54
 
53
55
  dependency = build_dependency(chart_file, parsed_line, dep["version"])
54
- dependency.requirements.map! do |req|
55
- req[:metadata] = {} unless req[:metadata]
56
- req[:metadata][:type] = :helm_chart
57
- req
58
- end
56
+ add_dependency_type_to_dependency(dependency, :helm_chart)
59
57
 
60
58
  dependency_set << dependency
61
59
  end
62
60
  end
63
61
 
62
+ sig { params(dependency: Dependabot::Dependency, type: Symbol).void }
63
+ def add_dependency_type_to_dependency(dependency, type)
64
+ dependency.requirements.map! do |req|
65
+ req[:metadata] = {} unless req[:metadata]
66
+ req[:metadata][:type] = type
67
+ req
68
+ end
69
+ end
70
+
71
+ sig { params(repository: T.nilable(String)).returns(String) }
72
+ def repository_from_registry(repository)
73
+ return DEFAULT_REPOSITORY if repository.nil?
74
+
75
+ repository
76
+ end
77
+
64
78
  sig { params(dependency_set: DependencySet).void }
65
79
  def parse_chart_yaml_files(dependency_set)
66
80
  helm_chart_files.each do |chart_file|
@@ -85,8 +99,7 @@ module Dependabot
85
99
  next unless version
86
100
 
87
101
  dependency = build_dependency(values_file, parsed_line, version)
88
- T.must(dependency.requirements.first)[:source] =
89
- T.must(dependency.requirements.first)[:source].merge(path: image_details[:path])
102
+ add_dependency_type_to_dependency(dependency, :docker_image)
90
103
 
91
104
  dependency_set << dependency
92
105
  end
@@ -0,0 +1,76 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+ require "dependabot/shared_helpers"
6
+ require "dependabot/dependency"
7
+ require "dependabot/shared/shared_file_updater"
8
+ require "fileutils"
9
+ require "tmpdir"
10
+
11
+ module Dependabot
12
+ module Helm
13
+ class FileUpdater < Dependabot::Shared::SharedFileUpdater
14
+ class ChartUpdater
15
+ extend T::Sig
16
+
17
+ sig do
18
+ params(
19
+ dependency: Dependabot::Dependency
20
+ ).void
21
+ end
22
+ def initialize(dependency:)
23
+ @dependency = dependency
24
+ end
25
+
26
+ sig { params(file: Dependabot::DependencyFile).returns(T.nilable(String)) }
27
+ def updated_chart_yaml_content(file)
28
+ content = file.content
29
+ yaml_obj = YAML.safe_load(T.must(content))
30
+
31
+ content = update_chart_dependencies(T.must(content), yaml_obj, file)
32
+
33
+ raise "Expected content to change!" if content == file.content
34
+
35
+ content
36
+ end
37
+
38
+ private
39
+
40
+ sig { returns(Dependabot::Dependency) }
41
+ attr_reader :dependency
42
+
43
+ sig do
44
+ params(content: String, yaml_obj: T::Hash[T.untyped, T.untyped],
45
+ file: Dependabot::DependencyFile).returns(String)
46
+ end
47
+ def update_chart_dependencies(content, yaml_obj, file)
48
+ if update_chart_dependency?(file) && yaml_obj["dependencies"]
49
+ yaml_obj["dependencies"].each do |dep|
50
+ next unless dep["name"] == dependency.name
51
+
52
+ old_version = dep["version"].to_s
53
+ new_version = dependency.version
54
+
55
+ pattern = /
56
+ (\s+-\s+name:\s+#{Regexp.escape(dependency.name)}.*?\n\s+)
57
+ (version:\s+)
58
+ ["']?#{Regexp.escape(old_version)}["']?
59
+ /mx
60
+ content = content.gsub(pattern) do |match|
61
+ match.gsub(/version: ["']?#{Regexp.escape(old_version)}["']?/, "version: #{new_version}")
62
+ end
63
+ end
64
+ end
65
+ content
66
+ end
67
+
68
+ sig { params(file: Dependabot::DependencyFile).returns(T::Boolean) }
69
+ def update_chart_dependency?(file)
70
+ reqs = dependency.requirements.select { |r| r[:file] == file.name }
71
+ reqs.any? { |r| r[:metadata]&.dig(:type) == :helm_chart }
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,116 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "dependabot/shared/shared_file_updater"
5
+ require "dependabot/dependency"
6
+ require "dependabot/dependency_file"
7
+ require "yaml"
8
+
9
+ module Dependabot
10
+ module Helm
11
+ class FileUpdater < Dependabot::Shared::SharedFileUpdater
12
+ class ImageUpdater
13
+ extend T::Sig
14
+ extend T::Helpers
15
+
16
+ sig { params(dependency: Dependency, dependency_files: T::Array[Dependabot::DependencyFile]).void }
17
+ def initialize(dependency:, dependency_files:)
18
+ @dependency_files = dependency_files
19
+ @dependency = dependency
20
+ end
21
+
22
+ sig { params(file_name: String).returns(T.nilable(String)) }
23
+ def updated_values_yaml_content(file_name)
24
+ value_file = dependency_files.find { |f| f.name.match?(file_name) }
25
+ raise "Expected a values.yaml file to exist!" if value_file.nil?
26
+
27
+ content = value_file.content
28
+ yaml_stream = YAML.parse_stream(T.must(content))
29
+
30
+ update_image_tags_recursive(yaml_stream, T.must(content))
31
+ end
32
+
33
+ private
34
+
35
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
36
+ attr_reader :dependency_files
37
+ sig { returns(Dependabot::Dependency) }
38
+ attr_reader :dependency
39
+
40
+ sig { params(yaml_stream: Psych::Nodes::Stream, content: String).returns(String) }
41
+ def update_image_tags_recursive(yaml_stream, content)
42
+ updated_content = content.dup.split("\n")
43
+
44
+ yaml_stream.children.each do |document|
45
+ document.children.each do |root_node|
46
+ updated_content = find_and_update_images(root_node, updated_content)
47
+ end
48
+ end
49
+
50
+ updated_content = updated_content.join("\n")
51
+
52
+ raise "Expected content to change!" if content == updated_content
53
+
54
+ updated_content
55
+ end
56
+
57
+ sig { params(node: Psych::Nodes::Node, content: T::Array[String]).returns(T::Array[String]) }
58
+ def find_and_update_images(node, content)
59
+ if node.is_a?(Psych::Nodes::Mapping)
60
+ content = process_mapping_node(node, content)
61
+ elsif node.is_a?(Psych::Nodes::Sequence)
62
+ content = process_sequence_node(node, content)
63
+ end
64
+
65
+ content
66
+ end
67
+
68
+ sig { params(node: Psych::Nodes::Node, content: T::Array[String]).returns(T::Array[String]) }
69
+ def process_mapping_node(node, content)
70
+ node.children.each_slice(2) do |key_node, value_node|
71
+ next unless key_node.is_a?(Psych::Nodes::Scalar)
72
+
73
+ key = key_node.value
74
+ content = process_image_key(key, value_node, content)
75
+
76
+ if value_node.is_a?(Psych::Nodes::Mapping) || value_node.is_a?(Psych::Nodes::Sequence)
77
+ content = find_and_update_images(value_node, content)
78
+ end
79
+ end
80
+ content
81
+ end
82
+
83
+ sig { params(node: Psych::Nodes::Node, content: T::Array[String]).returns(T::Array[String]) }
84
+ def process_sequence_node(node, content)
85
+ node.children.reduce(content) do |updated_content, child|
86
+ find_and_update_images(child, updated_content)
87
+ end
88
+ end
89
+
90
+ sig { params(key: String, value_node: Psych::Nodes::Node, content: T::Array[String]).returns(T::Array[String]) }
91
+ def process_image_key(key, value_node, content)
92
+ return content unless key == "image" && value_node.is_a?(Psych::Nodes::Mapping)
93
+
94
+ dependency_name = dependency.name
95
+ dependency_version = T.must(dependency.version)
96
+ dependency_requirements = dependency.requirements
97
+
98
+ has_dependency = value_node.children.any? { |n| n.value == dependency_name }
99
+ return content unless has_dependency
100
+
101
+ dependency_requirements.each do |req|
102
+ next unless req[:metadata][:type] == :docker_image
103
+
104
+ version_scalar = value_node.children.find { |n| n.value == req[:source][:tag] }
105
+ next unless version_scalar
106
+
107
+ line = version_scalar.start_line
108
+ content[line] = T.must(content[line]).gsub(req[:source][:tag], dependency_version)
109
+ end
110
+
111
+ content
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,63 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+ require "dependabot/shared_helpers"
6
+ require "dependabot/dependency"
7
+ require "dependabot/shared/shared_file_updater"
8
+ require "dependabot/helm/helpers"
9
+ require "fileutils"
10
+ require "tmpdir"
11
+
12
+ module Dependabot
13
+ module Helm
14
+ class FileUpdater < Dependabot::Shared::SharedFileUpdater
15
+ class LockFileGenerator
16
+ extend T::Sig
17
+
18
+ sig do
19
+ params(
20
+ dependencies: T::Array[Dependabot::Dependency],
21
+ dependency_files: T::Array[Dependabot::DependencyFile],
22
+ repo_contents_path: String,
23
+ credentials: T::Array[Dependabot::Credential]
24
+ ).void
25
+ end
26
+ def initialize(dependencies:, dependency_files:, repo_contents_path:, credentials:)
27
+ @dependencies = dependencies
28
+ @dependency_files = dependency_files
29
+ @repo_contents_path = repo_contents_path
30
+ @credentials = credentials
31
+ end
32
+
33
+ sig { params(chart_lock: Dependabot::DependencyFile, updated_content: String).returns(String) }
34
+ def updated_chart_lock(chart_lock, updated_content)
35
+ SharedHelpers.in_a_temporary_repo_directory(base_dir, repo_contents_path) do
36
+ SharedHelpers.with_git_configured(credentials: credentials) do
37
+ File.write("Chart.yaml", updated_content)
38
+ Helpers.update_lock
39
+
40
+ File.read(chart_lock.name)
41
+ end
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ sig { returns(T::Array[Dependabot::Dependency]) }
48
+ attr_reader :dependencies
49
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
50
+ attr_reader :dependency_files
51
+ sig { returns(String) }
52
+ attr_reader :repo_contents_path
53
+ sig { returns(T::Array[Dependabot::Credential]) }
54
+ attr_reader :credentials
55
+
56
+ sig { returns(String) }
57
+ def base_dir
58
+ T.must(dependency_files.first).directory
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -2,6 +2,9 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "dependabot/shared/shared_file_updater"
5
+ require "dependabot/helm/file_updater/lock_file_generator"
6
+ require "dependabot/helm/file_updater/image_updater"
7
+ require "dependabot/helm/file_updater/chart_updater"
5
8
  require "yaml"
6
9
 
7
10
  module Dependabot
@@ -11,6 +14,7 @@ module Dependabot
11
14
  extend T::Helpers
12
15
 
13
16
  CHART_YAML_REGEXP = /Chart\.ya?ml/i
17
+ CHART_LOCK_REGEXP = /Chart\.lock/i
14
18
  VALUES_YAML_REGEXP = /values(?>\.[\w-]+)?\.ya?ml/i
15
19
  YAML_REGEXP = /(Chart|values(?>\.[\w-]+)?)\.ya?ml/i
16
20
  IMAGE_REGEX = /(?:image:|repository:\s*)/i
@@ -47,14 +51,17 @@ module Dependabot
47
51
  next unless requirement_changed?(file, T.must(dependency))
48
52
 
49
53
  if file.name.match?(CHART_YAML_REGEXP)
54
+ updated_content = chart_updater.updated_chart_yaml_content(file)
50
55
  updated_files << updated_file(
51
56
  file: file,
52
- content: T.must(updated_chart_yaml_content(file))
57
+ content: T.must(updated_content)
53
58
  )
59
+
60
+ updated_files.concat(update_chart_locks(T.must(updated_content))) if chart_locks
54
61
  elsif file.name.match?(VALUES_YAML_REGEXP)
55
62
  updated_files << updated_file(
56
63
  file: file,
57
- content: T.must(updated_values_yaml_content(file))
64
+ content: T.must(image_updater.updated_values_yaml_content(file.name))
58
65
  )
59
66
  end
60
67
  end
@@ -67,141 +74,52 @@ module Dependabot
67
74
 
68
75
  private
69
76
 
70
- sig do
71
- params(content: String, yaml_obj: T::Hash[T.untyped, T.untyped],
72
- file: Dependabot::DependencyFile).returns(String)
73
- end
74
- def update_chart_dependencies(content, yaml_obj, file)
75
- if update_chart_dependency?(file)
76
- yaml_obj["dependencies"].each do |dep|
77
- next unless dep["name"] == T.must(dependency).name
78
-
79
- old_version = dep["version"].to_s
80
- new_version = T.must(dependency).version
81
-
82
- pattern = /
83
- (\s+-\sname:\s#{Regexp.escape(T.must(dependency).name)}.*?\n\s+version:\s)
84
- ["']?#{Regexp.escape(old_version)}["']?
85
- /mx
86
- content = content.gsub(pattern) do |match|
87
- match.gsub(/version: ["']?#{Regexp.escape(old_version)}["']?/, "version: #{new_version}")
88
- end
89
- end
77
+ sig { params(updated_content: String).returns(T::Array[Dependabot::DependencyFile]) }
78
+ def update_chart_locks(updated_content)
79
+ chart_locks.map do |chart_lock|
80
+ updated_file(
81
+ file: chart_lock,
82
+ content: updated_chart_lock_content(chart_lock, updated_content)
83
+ )
90
84
  end
91
- content
92
- end
93
-
94
- sig { params(file: Dependabot::DependencyFile).returns(T.nilable(String)) }
95
- def updated_chart_yaml_content(file)
96
- content = file.content
97
- yaml_obj = YAML.safe_load(T.must(content))
98
-
99
- content = update_chart_dependencies(T.must(content), yaml_obj, file)
100
-
101
- raise "Expected content to change!" if content == file.content
102
-
103
- content
104
- end
105
-
106
- sig { params(content: String, path: String, old_tag: String, new_tag: String).returns(String) }
107
- def update_tag(content, path, old_tag, new_tag)
108
- indent_pattern = get_indent_pattern(content, path)
109
- tag_pattern = /#{indent_pattern}tag:\s+["']?#{Regexp.escape(old_tag)}["']?/
110
- content.gsub(tag_pattern, "#{indent_pattern}tag: #{new_tag}")
111
85
  end
112
86
 
113
- sig { params(content: String, path: String, old_image: String, new_image: String).returns(String) }
114
- def update_image(content, path, old_image, new_image)
115
- indent_pattern = get_indent_pattern(content, path)
116
- image_pattern = /#{indent_pattern}image:\s+["']?#{Regexp.escape(old_image)}["']?/
117
- content.gsub(image_pattern, "#{indent_pattern}image: #{new_image}")
87
+ sig { returns(LockFileGenerator) }
88
+ def lockfile_updater
89
+ @lockfile_updater ||= T.let(LockFileGenerator.new(
90
+ dependencies: dependencies,
91
+ dependency_files: dependency_files,
92
+ repo_contents_path: T.must(repo_contents_path),
93
+ credentials: credentials
94
+ ), T.nilable(Dependabot::Helm::FileUpdater::LockFileGenerator))
118
95
  end
119
96
 
120
- sig { params(content: String, path_parts: T::Array[String]).returns(String) }
121
- def update_tag_in_content(content, path_parts)
122
- parent_path = T.must(path_parts[0...-1]).join(".")
123
- old_tag = T.must(dependency).previous_version
124
- new_tag = T.must(dependency).version
125
- update_tag(content, parent_path, T.must(old_tag), T.must(new_tag))
97
+ sig { returns(ImageUpdater) }
98
+ def image_updater
99
+ @image_updater ||= T.let(ImageUpdater.new(dependency: T.must(dependency), dependency_files: dependency_files),
100
+ T.nilable(Dependabot::Helm::FileUpdater::ImageUpdater))
126
101
  end
127
102
 
128
- sig { params(content: String, path: String, req: T::Hash[Symbol, T.untyped]).returns(String) }
129
- def update_image_in_content(content, path, req)
130
- old_image = build_old_image_string(req)
131
- new_image = build_new_image_string(req)
132
- update_image(content, path, old_image, new_image)
133
- end
134
-
135
- sig { params(file: Dependabot::DependencyFile).returns(T.nilable(String)) }
136
- def updated_values_yaml_content(file)
137
- content = file.content
138
- req = T.must(dependency).requirements.find { |r| r[:file] == file.name }
139
-
140
- if update_container_image?(file) && req&.dig(:source, :path)
141
- path = req.dig(:source, :path).to_s
142
- path_parts = path.split(".")
143
-
144
- content = if path_parts.last == "tag"
145
- update_tag_in_content(T.must(content), path_parts)
146
- elsif path_parts.last == "image"
147
- update_image_in_content(T.must(content), path, req)
148
- else
149
- content
150
- end
151
- end
152
-
153
- raise "Expected content to change!" if content == file.content
154
-
155
- content
156
- end
157
-
158
- sig { params(content: String, path: String).returns(String) }
159
- def get_indent_pattern(content, path)
160
- path_parts = path.split(".")
161
- indent = T.let("", T.untyped)
162
-
163
- path_parts.each do |part|
164
- pattern = /^(#{indent}\s*)#{part}:/
165
- if content.match(pattern)
166
- indent = T.must(T.must(content.match(pattern))[1]) + " " # Add 2 spaces for next level
167
- end
168
- end
169
-
170
- indent
171
- end
172
-
173
- sig { params(requirement: T::Hash[Symbol, T.untyped]).returns(String) }
174
- def build_old_image_string(requirement)
175
- old_source = requirement.fetch(:source)
176
- prefix = old_source[:registry] ? "#{old_source[:registry]}/" : ""
177
- name = T.must(dependency).name
178
- tag = T.must(dependency).previous_version
179
- digest = old_source[:digest] ? "@sha256:#{old_source[:digest]}" : ""
180
-
181
- "#{prefix}#{name}:#{tag}#{digest}"
182
- end
183
-
184
- sig { params(requirement: T::Hash[Symbol, T.untyped]).returns(String) }
185
- def build_new_image_string(requirement)
186
- new_source = requirement.fetch(:source)
187
- prefix = new_source[:registry] ? "#{new_source[:registry]}/" : ""
188
- name = T.must(dependency).name
189
- tag = T.must(dependency).version
190
- digest = new_source[:digest] ? "@sha256:#{new_source[:digest]}" : ""
191
-
192
- "#{prefix}#{name}:#{tag}#{digest}"
103
+ sig { returns(ChartUpdater) }
104
+ def chart_updater
105
+ @chart_updater ||= T.let(ChartUpdater.new(dependency: T.must(dependency)),
106
+ T.nilable(Dependabot::Helm::FileUpdater::ChartUpdater))
193
107
  end
194
108
 
195
- sig { params(file: Dependabot::DependencyFile).returns(T::Boolean) }
196
- def update_chart_dependency?(file)
197
- reqs = T.must(dependency).requirements.select { |r| r[:file] == file.name }
198
- reqs.any? { |r| r[:metadata]&.dig(:type) == :helm_chart }
109
+ sig { params(chart_lock: Dependabot::DependencyFile, updated_content: String).returns(String) }
110
+ def updated_chart_lock_content(chart_lock, updated_content)
111
+ @updated_chart_lock_content ||= T.let({}, T.nilable(T::Hash[String, T.nilable(String)]))
112
+ @updated_chart_lock_content[chart_lock.name] ||=
113
+ lockfile_updater.updated_chart_lock(chart_lock, updated_content)
199
114
  end
200
115
 
201
- sig { params(file: Dependabot::DependencyFile).returns(T::Boolean) }
202
- def update_container_image?(file)
203
- reqs = T.must(dependency).requirements.select { |r| r[:file] == file.name }
204
- reqs.any? { |r| r[:groups]&.include?("image") }
116
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
117
+ def chart_locks
118
+ @chart_locks ||= T.let(
119
+ dependency_files
120
+ .select { |f| f.name.match(CHART_LOCK_REGEXP) },
121
+ T.nilable(T::Array[Dependabot::DependencyFile])
122
+ )
205
123
  end
206
124
  end
207
125
  end
@@ -0,0 +1,56 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "dependabot/dependency"
5
+ require "dependabot/file_parsers"
6
+ require "dependabot/file_parsers/base"
7
+ require "dependabot/shared_helpers"
8
+ require "sorbet-runtime"
9
+
10
+ module Dependabot
11
+ module Helm
12
+ module Helpers
13
+ extend T::Sig
14
+
15
+ sig { params(name: String).returns(String) }
16
+ def self.search_releases(name)
17
+ Dependabot.logger.info("Searching Helm repository for: #{name}")
18
+
19
+ Dependabot::SharedHelpers.run_shell_command(
20
+ "helm search repo #{name} --versions --output=json",
21
+ fingerprint: "helm search repo <name> --versions --output=json"
22
+ ).strip
23
+ end
24
+
25
+ sig { params(repo_name: String, repo_url: String).returns(String) }
26
+ def self.add_repo(repo_name, repo_url)
27
+ Dependabot.logger.info("Adding Helm repository: #{repo_name} (#{repo_url})")
28
+
29
+ Dependabot::SharedHelpers.run_shell_command(
30
+ "helm repo add #{repo_name} #{repo_url}",
31
+ fingerprint: "helm repo add <repo_name> <repo_url>"
32
+ )
33
+ end
34
+
35
+ sig { returns(String) }
36
+ def self.update_repo
37
+ Dependabot.logger.info("Updating Helm repositories")
38
+
39
+ Dependabot::SharedHelpers.run_shell_command(
40
+ "helm repo update",
41
+ fingerprint: "helm repo update"
42
+ )
43
+ end
44
+
45
+ sig { returns(String) }
46
+ def self.update_lock
47
+ Dependabot.logger.info("Updating Building Lock File")
48
+
49
+ Dependabot::SharedHelpers.run_shell_command(
50
+ "helm dependency update",
51
+ fingerprint: "helm dependency update"
52
+ )
53
+ end
54
+ end
55
+ end
56
+ end
@@ -8,8 +8,11 @@ require "dependabot/errors"
8
8
  require "dependabot/docker/version"
9
9
  require "dependabot/docker/requirement"
10
10
  require "dependabot/shared/utils/credentials_finder"
11
+ require "dependabot/shared_helpers"
11
12
  require "excon"
12
13
  require "yaml"
14
+ require "json"
15
+ require "dependabot/helm/helpers"
13
16
 
14
17
  module Dependabot
15
18
  module Helm
@@ -38,10 +41,7 @@ module Dependabot
38
41
  dependency.requirements.map do |req|
39
42
  updated_metadata = req.fetch(:metadata).dup
40
43
  updated_req = req.dup
41
- if updated_metadata.key?(:type) && updated_metadata[:type] == :helm_chart
42
- updated_req[:requirement] = latest_version.to_s
43
- updated_req[:source][:tag] = latest_version.to_s
44
- end
44
+ updated_req[:requirement] = latest_version.to_s if updated_metadata.key?(:type)
45
45
 
46
46
  updated_req
47
47
  end
@@ -49,6 +49,68 @@ module Dependabot
49
49
 
50
50
  private
51
51
 
52
+ sig { override.returns(T::Array[Dependabot::Dependency]) }
53
+ def updated_dependencies_after_full_unlock
54
+ raise NotImplementedError
55
+ end
56
+
57
+ sig do
58
+ params(chart_name: String, repo_name: T.nilable(String),
59
+ repo_url: T.nilable(String)).returns(T.nilable(Gem::Version))
60
+ end
61
+ def fetch_releases_with_helm_cli(chart_name, repo_name, repo_url)
62
+ Dependabot.logger.info("Attempting to search for #{chart_name} using helm CLI")
63
+ releases = fetch_chart_releases(chart_name, repo_name, repo_url)
64
+
65
+ return nil unless releases && !releases.empty?
66
+
67
+ valid_releases = filter_valid_releases(releases)
68
+ return nil if valid_releases.empty?
69
+
70
+ highest_release = valid_releases.max_by { |release| version_class.new(release["version"]) }
71
+ Dependabot.logger.info(
72
+ "Found latest version #{T.must(highest_release)['version']} for #{chart_name} using helm search"
73
+ )
74
+ version_class.new(T.must(highest_release)["version"])
75
+ end
76
+
77
+ sig { params(chart_name: String, repo_url: T.nilable(String)).returns(T.nilable(Gem::Version)) }
78
+ def fetch_releases_from_index(chart_name, repo_url)
79
+ Dependabot.logger.info("Falling back to index.yaml search for #{chart_name}")
80
+ return nil unless repo_url
81
+
82
+ index_url = build_index_url(repo_url)
83
+ index = fetch_helm_chart_index(index_url)
84
+ return nil unless index && index["entries"] && index["entries"][chart_name]
85
+
86
+ all_versions = index["entries"][chart_name].map { |entry| entry["version"] }
87
+ Dependabot.logger.info("Found #{all_versions.length} versions for #{chart_name} in index.yaml")
88
+
89
+ valid_versions = filter_valid_versions(all_versions)
90
+ Dependabot.logger.info("After filtering, found #{valid_versions.length} valid versions for #{chart_name}")
91
+
92
+ return nil if valid_versions.empty?
93
+
94
+ highest_version = valid_versions.map { |v| version_class.new(v) }.max
95
+ Dependabot.logger.info("Highest valid version for #{chart_name} is #{highest_version}")
96
+
97
+ highest_version
98
+ end
99
+
100
+ sig { params(releases: T::Array[T::Hash[String, T.untyped]]).returns(T::Array[T::Hash[String, T.untyped]]) }
101
+ def filter_valid_releases(releases)
102
+ releases.reject do |release|
103
+ version_class.new(release["version"]) <= version_class.new(dependency.version) ||
104
+ ignore_requirements.any? { |r| r.satisfied_by?(version_class.new(release["version"])) }
105
+ end
106
+ end
107
+
108
+ sig { params(repo_url: String).returns(String) }
109
+ def build_index_url(repo_url)
110
+ repo_url_trimmed = repo_url.to_s.strip.chomp("/")
111
+ "#{repo_url_trimmed}/index.yaml"
112
+ end
113
+
52
114
  sig { override.returns(T::Boolean) }
53
115
  def latest_version_resolvable_with_full_unlock?
54
116
  false
@@ -64,9 +126,9 @@ module Dependabot
64
126
  sig { returns(T.nilable(T.any(String, Gem::Version))) }
65
127
  def fetch_latest_version
66
128
  case dependency_type
67
- when :chart_dependency
129
+ when :helm_chart
68
130
  fetch_latest_chart_version
69
- when :image_reference
131
+ when :docker_image
70
132
  fetch_latest_image_version
71
133
  else
72
134
  Gem::Version.new(dependency.version)
@@ -76,11 +138,66 @@ module Dependabot
76
138
  sig { returns(Symbol) }
77
139
  def dependency_type
78
140
  req = dependency.requirements.first
141
+ type = T.must(req).dig(:metadata, :type)
79
142
 
80
- return :image_reference if T.must(req)[:groups]&.include?("image")
81
- return :chart_dependency if T.must(req).dig(:metadata, :type) == :helm_chart
143
+ type || :unknown
144
+ end
82
145
 
83
- :unknown
146
+ sig do
147
+ params(chart_name: String, repo_name: T.nilable(String),
148
+ repo_url: T.nilable(String)).returns(T.nilable(T::Array[T::Hash[String, T.untyped]]))
149
+ end
150
+ def fetch_chart_releases(chart_name, repo_name = nil, repo_url = nil)
151
+ Dependabot.logger.info("Fetching releases for Helm chart: #{chart_name}")
152
+
153
+ if repo_name && repo_url
154
+ begin
155
+ Helpers.add_repo(repo_name, repo_url)
156
+ Helpers.update_repo
157
+ rescue StandardError => e
158
+ Dependabot.logger.error("Error adding/updating Helm repository: #{e.message}")
159
+ end
160
+ end
161
+
162
+ begin
163
+ search_command = repo_name ? "#{repo_name}/#{chart_name}" : chart_name
164
+ Dependabot.logger.info("Searching for: #{search_command}")
165
+
166
+ json_output = Helpers.search_releases(search_command)
167
+ return nil if json_output.empty?
168
+
169
+ releases = JSON.parse(json_output)
170
+ Dependabot.logger.info("Found #{releases.length} releases for #{chart_name}")
171
+ releases
172
+ rescue StandardError => e
173
+ Dependabot.logger.error("Error fetching chart releases: #{e.message}")
174
+ nil
175
+ end
176
+ end
177
+
178
+ sig { returns(T.nilable(Gem::Version)) }
179
+ def fetch_latest_chart_version
180
+ chart_name = dependency.name
181
+ source = dependency.requirements.first&.dig(:source)
182
+ repo_url = source&.dig(:registry)
183
+ repo_name = extract_repo_name(repo_url)
184
+
185
+ releases = fetch_releases_with_helm_cli(chart_name, repo_name, repo_url)
186
+ return releases if releases
187
+
188
+ fetch_releases_from_index(chart_name, repo_url)
189
+ end
190
+
191
+ sig { params(repo_url: T.nilable(String)).returns(T.nilable(String)) }
192
+ def extract_repo_name(repo_url)
193
+ return nil unless repo_url
194
+
195
+ name = repo_url.gsub(%r{^https?://}, "")
196
+ name = name.chomp("/")
197
+ name = name.gsub(/[^a-zA-Z0-9-]/, "-")
198
+ name = "repo-#{name}" unless name.match?(/^[a-zA-Z0-9]/)
199
+
200
+ name
84
201
  end
85
202
 
86
203
  sig { params(index_url: String).returns(T.nilable(T::Hash[T.untyped, T.untyped])) }
@@ -113,32 +230,6 @@ module Dependabot
113
230
  end
114
231
 
115
232
  sig { returns(T.nilable(Gem::Version)) }
116
- def fetch_latest_chart_version
117
- source_url = dependency.requirements.first&.dig(:source, :registry)
118
- return nil unless source_url
119
-
120
- repo_url = source_url.to_s.strip.chomp("/")
121
- chart_name = dependency.name
122
- index_url = "#{repo_url}/index.yaml"
123
-
124
- index = fetch_helm_chart_index(index_url)
125
- return nil unless index && index["entries"] && index["entries"][chart_name]
126
-
127
- all_versions = index["entries"][chart_name].map { |entry| entry["version"] }
128
- Dependabot.logger.info("Found #{all_versions.length} versions for #{chart_name}")
129
-
130
- valid_versions = filter_valid_versions(all_versions)
131
- Dependabot.logger.info("After filtering, found #{valid_versions.length} valid versions for #{chart_name}")
132
-
133
- return nil if valid_versions.empty?
134
-
135
- highest_version = valid_versions.map { |v| version_class.new(v) }.max
136
- Dependabot.logger.info("Highest valid version for #{chart_name} is #{highest_version}")
137
-
138
- highest_version
139
- end
140
-
141
- sig { returns(T.nilable(String)) }
142
233
  def fetch_latest_image_version
143
234
  docker_dependency = build_docker_dependency
144
235
 
@@ -153,12 +244,11 @@ module Dependabot
153
244
  raise_on_ignored: raise_on_ignored
154
245
  )
155
246
 
156
- latest = docker_checker.latest_version
157
- latest_version_str = latest&.to_s
247
+ latest_version = docker_checker.latest_version
158
248
 
159
- Dependabot.logger.info("Docker UpdateChecker found latest version: #{latest_version_str || 'none'}")
249
+ Dependabot.logger.info("Docker UpdateChecker found latest version: #{latest_version || 'none'}")
160
250
 
161
- latest_version_str
251
+ version_class.new(latest_version)
162
252
  end
163
253
 
164
254
  sig { returns(Dependabot::Dependency) }
@@ -175,7 +265,7 @@ module Dependabot
175
265
  end
176
266
  end
177
267
 
178
- registry = source[:registry] || "docker.io"
268
+ registry = source[:registry] || nil
179
269
 
180
270
  Dependency.new(
181
271
  name: name,
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dependabot-helm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.302.0
4
+ version: 0.303.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dependabot
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-03-20 00:00:00.000000000 Z
11
+ date: 2025-03-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dependabot-common
@@ -16,28 +16,28 @@ dependencies:
16
16
  requirements:
17
17
  - - '='
18
18
  - !ruby/object:Gem::Version
19
- version: 0.302.0
19
+ version: 0.303.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - '='
25
25
  - !ruby/object:Gem::Version
26
- version: 0.302.0
26
+ version: 0.303.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: dependabot-docker
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - '='
32
32
  - !ruby/object:Gem::Version
33
- version: 0.302.0
33
+ version: 0.303.0
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - '='
39
39
  - !ruby/object:Gem::Version
40
- version: 0.302.0
40
+ version: 0.303.0
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: debug
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -170,14 +170,14 @@ dependencies:
170
170
  requirements:
171
171
  - - "~>"
172
172
  - !ruby/object:Gem::Version
173
- version: 0.8.5
173
+ version: 0.8.7
174
174
  type: :development
175
175
  prerelease: false
176
176
  version_requirements: !ruby/object:Gem::Requirement
177
177
  requirements:
178
178
  - - "~>"
179
179
  - !ruby/object:Gem::Version
180
- version: 0.8.5
180
+ version: 0.8.7
181
181
  - !ruby/object:Gem::Dependency
182
182
  name: simplecov
183
183
  requirement: !ruby/object:Gem::Requirement
@@ -260,6 +260,10 @@ files:
260
260
  - lib/dependabot/helm/file_fetcher.rb
261
261
  - lib/dependabot/helm/file_parser.rb
262
262
  - lib/dependabot/helm/file_updater.rb
263
+ - lib/dependabot/helm/file_updater/chart_updater.rb
264
+ - lib/dependabot/helm/file_updater/image_updater.rb
265
+ - lib/dependabot/helm/file_updater/lock_file_generator.rb
266
+ - lib/dependabot/helm/helpers.rb
263
267
  - lib/dependabot/helm/package_manager.rb
264
268
  - lib/dependabot/helm/update_checker.rb
265
269
  homepage: https://github.com/dependabot/dependabot-core
@@ -267,7 +271,7 @@ licenses:
267
271
  - MIT
268
272
  metadata:
269
273
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
270
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.302.0
274
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.303.0
271
275
  post_install_message:
272
276
  rdoc_options: []
273
277
  require_paths: