dependabot-opentofu 0.348.1

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.
@@ -0,0 +1,223 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ ####################################################################
5
+ # For more details on OpenTofu version constraints, see: #
6
+ # https://opentofu.org/docs/language/modules/#published-modules #
7
+ ####################################################################
8
+
9
+ require "sorbet-runtime"
10
+
11
+ require "dependabot/opentofu/version"
12
+ require "dependabot/opentofu/requirement"
13
+
14
+ module Dependabot
15
+ module Opentofu
16
+ # Takes an array of `requirements` hashes for a dependency at the old
17
+ # version and a new version, and generates a set of new `requirements`
18
+ # hashes at the new version.
19
+ #
20
+ # A requirements hash is a basic description of a dependency at a certain
21
+ # version constraint, and it includes the data that is needed to update the
22
+ # manifest (i.e. the `.tf` file) with the new version.
23
+ #
24
+ # A requirements hash looks like this for a registry hosted requirement:
25
+ # ```ruby
26
+ # {
27
+ # requirement: "~> 0.2.1",
28
+ # groups: [],
29
+ # file: "main.tf",
30
+ # source: {
31
+ # type: "registry",
32
+ # registry_hostname: "registry.opentofu.org",
33
+ # module_identifier: "hashicorp/consul/aws"
34
+ # }
35
+ # }
36
+ #
37
+ # And like this for a git requirement:
38
+ # ```ruby
39
+ # {
40
+ # requirement: nil,
41
+ # groups: [],
42
+ # file: "main.tf",
43
+ # source: {
44
+ # type: "git",
45
+ # url: "https://github.com/cloudposse/terraform-null-label.git",
46
+ # branch: nil,
47
+ # ref: nil
48
+ # }
49
+ # }
50
+ class RequirementsUpdater
51
+ extend T::Sig
52
+
53
+ # @param requirements [Hash{Symbol => String, Array, Hash}]
54
+ # @param latest_version [Dependabot::Opentofu::Version]
55
+ # @param tag_for_latest_version [String, NilClass]
56
+ sig do
57
+ params(
58
+ requirements: T::Array[T::Hash[Symbol, T.untyped]],
59
+ latest_version: T.nilable(Dependabot::Version::VersionParameter),
60
+ tag_for_latest_version: T.nilable(String)
61
+ ).void
62
+ end
63
+ def initialize(requirements:, latest_version:, tag_for_latest_version:)
64
+ @requirements = requirements
65
+ @tag_for_latest_version = tag_for_latest_version
66
+
67
+ return unless latest_version
68
+ return unless version_class.correct?(latest_version)
69
+
70
+ @latest_version = T.let(version_class.new(latest_version), Dependabot::Opentofu::Version)
71
+ end
72
+
73
+ # @return requirements [Hash{Symbol => String, Array, Hash}]
74
+ # * requirement [String, NilClass] the updated version constraint
75
+ # * groups [Array] no-op for OpenTofu
76
+ # * file [String] the file that specified this dependency
77
+ # * source [Hash{Symbol => String}] The updated git or registry source details
78
+ sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
79
+ def updated_requirements
80
+ # NOTE: Order is important here. The FileUpdater needs the updated
81
+ # requirement at index `i` to correspond to the previous requirement
82
+ # at the same index.
83
+ requirements.map do |req|
84
+ case req.dig(:source, :type)
85
+ when "git" then update_git_requirement(req)
86
+ when "registry", "provider" then update_registry_requirement(req)
87
+ else req
88
+ end
89
+ end
90
+ end
91
+
92
+ private
93
+
94
+ sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
95
+ attr_reader :requirements
96
+
97
+ sig { returns(Dependabot::Opentofu::Version) }
98
+ attr_reader :latest_version
99
+
100
+ sig { returns(T.nilable(String)) }
101
+ attr_reader :tag_for_latest_version
102
+
103
+ sig { params(req: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
104
+ def update_git_requirement(req)
105
+ return req unless req.dig(:source, :ref)
106
+ return req unless tag_for_latest_version
107
+
108
+ req.merge(source: req[:source].merge(ref: tag_for_latest_version))
109
+ end
110
+
111
+ sig { params(req: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
112
+ def update_registry_requirement(req)
113
+ return req if req.fetch(:requirement).nil?
114
+
115
+ string_req = req.fetch(:requirement).strip
116
+ ruby_req = requirement_class.new(string_req)
117
+ return req if ruby_req.satisfied_by?(latest_version)
118
+
119
+ new_req =
120
+ if ruby_req.exact? then latest_version.to_s
121
+ elsif string_req.start_with?("~>")
122
+ update_twiddle_version(string_req).to_s
123
+ else
124
+ update_range(string_req).map(&:to_s).join(", ")
125
+ end
126
+
127
+ req.merge(requirement: new_req)
128
+ end
129
+
130
+ # Updates the version in a "~>" constraint to allow the given version
131
+ sig { params(req_string: String).returns(String) }
132
+ def update_twiddle_version(req_string)
133
+ old_version = requirement_class.new(req_string)
134
+ .requirements.first.last
135
+ updated_version = at_same_precision(latest_version, old_version)
136
+ req_string.sub(old_version.to_s, updated_version)
137
+ end
138
+
139
+ sig { params(req_string: String).returns(T::Array[Dependabot::Opentofu::Requirement]) }
140
+ def update_range(req_string)
141
+ requirement_class.new(req_string).requirements.flat_map do |r|
142
+ ruby_req = requirement_class.new(r.join(" "))
143
+ next ruby_req if ruby_req.satisfied_by?(latest_version)
144
+
145
+ case op = ruby_req.requirements.first.first
146
+ when "<", "<=" then [update_greatest_version(ruby_req, latest_version)]
147
+ when "!=" then []
148
+ else raise "Unexpected operation for unsatisfied req: #{op}"
149
+ end
150
+ end
151
+ end
152
+
153
+ sig do
154
+ params(
155
+ new_version: Dependabot::Opentofu::Version,
156
+ old_version: Dependabot::Opentofu::Version
157
+ )
158
+ .returns(String)
159
+ end
160
+ def at_same_precision(new_version, old_version)
161
+ release_precision =
162
+ old_version.to_s.split(".").count { |i| i.match?(/^\d+$/) }
163
+ prerelease_precision =
164
+ old_version.to_s.split(".").count - release_precision
165
+
166
+ new_release =
167
+ new_version.to_s.split(".").first(release_precision)
168
+ new_prerelease =
169
+ new_version.to_s.split(".")
170
+ .drop_while { |i| i.match?(/^\d+$/) }
171
+ .first([prerelease_precision, 1].max)
172
+
173
+ [*new_release, *new_prerelease].join(".")
174
+ end
175
+
176
+ # Updates the version in a "<" or "<=" constraint to allow the given
177
+ # version
178
+ sig do
179
+ params(
180
+ requirement: Dependabot::Requirement,
181
+ version_to_be_permitted: T.any(String, Dependabot::Opentofu::Version)
182
+ )
183
+ .returns(Dependabot::Opentofu::Requirement)
184
+ end
185
+ def update_greatest_version(requirement, version_to_be_permitted)
186
+ if version_to_be_permitted.is_a?(String)
187
+ version_to_be_permitted =
188
+ version_class.new(version_to_be_permitted)
189
+ end
190
+ op, version = requirement.requirements.first
191
+ version = version.release if version.prerelease?
192
+
193
+ # When 'less than'/'<',
194
+ # increment the last available segment only so that the new version is within the constraint
195
+ if op == "<"
196
+ new_segments = version.segments.map.with_index do |_, index|
197
+ version_to_be_permitted.segments[index]
198
+ end
199
+ new_segments[-1] += 1
200
+ # When 'less-than/equal'/'<=', use the new version as-is even when previously set as a non-semver version
201
+ # OpenTofu treats shortened versions the same as a version with any remaining segments as 0
202
+ # Example: '0.2' is treated as '0.2.0' | '1' is treated as '1.0.0'
203
+ elsif op == "<="
204
+ new_segments = version_to_be_permitted.segments
205
+ else
206
+ raise "Unexpected operation: #{op}"
207
+ end
208
+
209
+ requirement_class.new("#{op} #{new_segments.join('.')}")
210
+ end
211
+
212
+ sig { returns(T.class_of(Dependabot::Opentofu::Version)) }
213
+ def version_class
214
+ Version
215
+ end
216
+
217
+ sig { returns(T.class_of(Dependabot::Opentofu::Requirement)) }
218
+ def requirement_class
219
+ Requirement
220
+ end
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,217 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "dependabot/update_checkers/base"
5
+ require "dependabot/opentofu/package/package_details_fetcher"
6
+ require "sorbet-runtime"
7
+ require "dependabot/git_commit_checker"
8
+
9
+ module Dependabot
10
+ module Opentofu
11
+ class UpdateChecker < Dependabot::UpdateCheckers::Base
12
+ class LatestVersionResolver
13
+ extend T::Sig
14
+
15
+ DAY_IN_SECONDS = T.let(24 * 60 * 60, Integer)
16
+
17
+ sig do
18
+ params(
19
+ dependency: Dependabot::Dependency,
20
+ credentials: T::Array[Dependabot::Credential],
21
+ cooldown_options: T.nilable(Dependabot::Package::ReleaseCooldownOptions),
22
+ git_commit_checker: Dependabot::GitCommitChecker
23
+ ).void
24
+ end
25
+ def initialize(dependency:, credentials:, cooldown_options:, git_commit_checker:)
26
+ @dependency = dependency
27
+ @credentials = credentials
28
+ @cooldown_options = cooldown_options
29
+ @git_commit_checker = T.let(
30
+ git_commit_checker,
31
+ Dependabot::GitCommitChecker
32
+ )
33
+ end
34
+
35
+ sig { returns(Dependabot::Dependency) }
36
+ attr_reader :dependency
37
+
38
+ # Return latest version tag for the dependency, it removes tags that are in cooldown period
39
+ # and returns the latest version tag that is not in cooldown period. If exception occurs
40
+ # it will return the latest version tag from the git_commit_checker. as it was before
41
+ sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) }
42
+ def latest_version_tag
43
+ # step one fetch allowed version tags and
44
+ allowed_version_tags = git_commit_checker.allowed_version_tags
45
+ begin
46
+ if cooldown_enabled?
47
+ # sort the allowed version tags by name in descending order
48
+ select_version_tags_in_cooldown_period&.each do |tag_name|
49
+ # filter out if name is not in cooldown period
50
+ allowed_version_tags.reject! do |gitref_filtered|
51
+ true if gitref_filtered.name == tag_name
52
+ end
53
+ end
54
+ end
55
+ Dependabot.logger.info(
56
+ "Allowed version tags after filtering versions in cooldown:
57
+ #{allowed_version_tags.map(&:name).join(', ')}"
58
+ )
59
+ git_commit_checker.max_local_tag(allowed_version_tags)
60
+ rescue StandardError => e
61
+ Dependabot.logger.error("Error fetching latest version tag: #{e.message}")
62
+ git_commit_checker.local_tag_for_latest_version
63
+ end
64
+ end
65
+
66
+ # To filter versions in cooldown period based on version tags from registry call
67
+ sig do
68
+ params(versions: T::Array[Dependabot::Opentofu::Version])
69
+ .returns(T::Array[Dependabot::Opentofu::Version])
70
+ end
71
+ def filter_versions_in_cooldown_period_from_provider(versions)
72
+ # to make call for registry to get the versions
73
+ # step one fetch allowed version tags and
74
+
75
+ # sort the allowed version tags by name in descending order
76
+ select_tags_which_in_cooldown_from_provider&.each do |tag_name|
77
+ # Iterate through versions and filter out those matching the tag_name
78
+ versions.reject! do |version|
79
+ version.to_s == tag_name
80
+ end
81
+ end
82
+ Dependabot.logger.info(
83
+ "Allowed version tags after filtering versions in cooldown:
84
+ #{versions.map(&:to_s).join(', ')}"
85
+ )
86
+ versions
87
+ rescue StandardError => e
88
+ Dependabot.logger.error("Error filter_versions_in_cooldown_period_from_provider(versions): #{e.message}")
89
+ versions
90
+ end
91
+
92
+ # To filter versions in cooldown period based on version tags from registry call
93
+ sig do
94
+ params(versions: T::Array[Dependabot::Opentofu::Version])
95
+ .returns(T::Array[Dependabot::Opentofu::Version])
96
+ end
97
+ def filter_versions_in_cooldown_period_from_module(versions)
98
+ # to make call for registry to get the versions
99
+ # step one fetch allowed version tags and
100
+
101
+ # sort the allowed version tags by name in descending order
102
+ select_tags_which_in_cooldown_from_module&.each do |tag_name|
103
+ # Iterate through versions and filter out those matching the tag_name
104
+ versions.reject! do |version|
105
+ version.to_s == tag_name
106
+ end
107
+ end
108
+ Dependabot.logger.info(
109
+ "filter_versions_in_cooldown_period_from_module::
110
+ Allowed version tags after filtering versions in cooldown:#{versions.map(&:to_s).join(', ')}"
111
+ )
112
+ versions
113
+ rescue StandardError => e
114
+ Dependabot.logger.error("Error fetching latest version tag: #{e.message}")
115
+ versions
116
+ end
117
+
118
+ sig { returns(T.nilable(T::Array[String])) }
119
+ def select_version_tags_in_cooldown_period
120
+ version_tags_in_cooldown_period = T.let([], T::Array[String])
121
+
122
+ package_details_fetcher.fetch_tag_and_release_date.each do |git_tag_with_detail|
123
+ if check_if_version_in_cooldown_period?(T.must(git_tag_with_detail.release_date))
124
+ version_tags_in_cooldown_period << git_tag_with_detail.tag
125
+ end
126
+ end
127
+ version_tags_in_cooldown_period
128
+ rescue StandardError => e
129
+ Dependabot.logger.error("Error checking if version is in cooldown: #{e.message}")
130
+ version_tags_in_cooldown_period
131
+ end
132
+
133
+ sig { params(release_date: String).returns(T::Boolean) }
134
+ def check_if_version_in_cooldown_period?(release_date)
135
+ return false unless release_date.length.positive?
136
+
137
+ cooldown = @cooldown_options
138
+ return false unless cooldown
139
+
140
+ return false if cooldown.nil?
141
+
142
+ # Calculate the number of seconds passed since the release
143
+ passed_seconds = Time.now.to_i - release_date_to_seconds(release_date)
144
+ # Check if the release is within the cooldown period
145
+ passed_seconds < cooldown.default_days * DAY_IN_SECONDS
146
+ end
147
+
148
+ sig { params(release_date: String).returns(Integer) }
149
+ def release_date_to_seconds(release_date)
150
+ Time.parse(release_date).to_i
151
+ rescue ArgumentError => e
152
+ Dependabot.logger.error("Invalid release date format: #{release_date} and error: #{e.message}")
153
+ 0 # Default to 360 days in seconds if parsing fails, so that it will not be in cooldown
154
+ end
155
+
156
+ sig { returns(T.nilable(T::Array[String])) }
157
+ def select_tags_which_in_cooldown_from_provider
158
+ version_tags_in_cooldown_from_provider = T.let([], T::Array[String])
159
+
160
+ package_details_fetcher.fetch_tag_and_release_date_from_provider.each do |git_tag_with_detail|
161
+ if check_if_version_in_cooldown_period?(T.must(git_tag_with_detail.release_date))
162
+ version_tags_in_cooldown_from_provider << git_tag_with_detail.tag
163
+ end
164
+ end
165
+ version_tags_in_cooldown_from_provider
166
+ rescue StandardError => e
167
+ Dependabot.logger.error("Error checking if version is in cooldown: #{e.message}")
168
+ version_tags_in_cooldown_from_provider
169
+ end
170
+
171
+ sig { returns(T.nilable(T::Array[String])) }
172
+ def select_tags_which_in_cooldown_from_module
173
+ version_tags_in_cooldown_from_module = T.let([], T::Array[String])
174
+
175
+ package_details_fetcher.fetch_tag_and_release_date_from_module.each do |git_tag_with_detail|
176
+ if check_if_version_in_cooldown_period?(T.must(git_tag_with_detail.release_date))
177
+ version_tags_in_cooldown_from_module << git_tag_with_detail.tag
178
+ end
179
+ end
180
+ version_tags_in_cooldown_from_module
181
+ rescue StandardError => e
182
+ Dependabot.logger.error("Error checking if version is in cooldown: #{e.message}")
183
+ version_tags_in_cooldown_from_module
184
+ end
185
+
186
+ sig { returns(Package::PackageDetailsFetcher) }
187
+ def package_details_fetcher
188
+ @package_details_fetcher ||= T.let(
189
+ Package::PackageDetailsFetcher.new(
190
+ dependency: dependency,
191
+ credentials: credentials,
192
+ git_commit_checker: git_commit_checker
193
+ ),
194
+ T.nilable(Package::PackageDetailsFetcher)
195
+ )
196
+ end
197
+
198
+ sig { returns(T::Boolean) }
199
+ def cooldown_enabled?
200
+ # This is a simple check to see if user has put cooldown days.
201
+ # If not set, then we aassume user does not want cooldown.
202
+ # Since OpenTofu does not support Semver versioning, So option left
203
+ # for the user is to set cooldown default days.
204
+ return false if @cooldown_options.nil?
205
+
206
+ @cooldown_options.default_days.positive?
207
+ end
208
+
209
+ sig { returns(Dependabot::GitCommitChecker) }
210
+ attr_reader :git_commit_checker
211
+
212
+ sig { returns(T::Array[Dependabot::Credential]) }
213
+ attr_reader :credentials
214
+ end
215
+ end
216
+ end
217
+ end
@@ -0,0 +1,264 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ require "dependabot/update_checkers"
7
+ require "dependabot/update_checkers/base"
8
+ require "dependabot/git_commit_checker"
9
+ require "dependabot/opentofu/requirements_updater"
10
+ require "dependabot/opentofu/requirement"
11
+ require "dependabot/opentofu/version"
12
+ require "dependabot/opentofu/registry_client"
13
+
14
+ module Dependabot
15
+ module Opentofu
16
+ class UpdateChecker < Dependabot::UpdateCheckers::Base
17
+ extend T::Sig
18
+
19
+ require_relative "update_checker/latest_version_resolver"
20
+
21
+ ELIGIBLE_SOURCE_TYPES = T.let(
22
+ %w(git provider registry).freeze,
23
+ T::Array[String]
24
+ )
25
+
26
+ sig { override.returns(T.nilable(T.any(String, Gem::Version))) }
27
+ def latest_version
28
+ return latest_version_for_git_dependency if git_dependency?
29
+ return latest_version_for_registry_dependency if registry_dependency?
30
+
31
+ latest_version_for_provider_dependency if provider_dependency?
32
+ # Other sources (mercurial, path dependencies) just return `nil`
33
+ end
34
+
35
+ sig { override.returns(T.nilable(T.any(String, Gem::Version))) }
36
+ def latest_resolvable_version
37
+ # No concept of resolvability for terraform modules (that we're aware
38
+ # of - there may be in future).
39
+ latest_version
40
+ end
41
+
42
+ sig { override.returns(T.nilable(T.any(String, Dependabot::Version))) }
43
+ def latest_resolvable_version_with_no_unlock
44
+ # TODO: Update later to use lock files
45
+ nil
46
+ end
47
+
48
+ sig { override.returns(T::Array[T::Hash[Symbol, T.untyped]]) }
49
+ def updated_requirements
50
+ RequirementsUpdater.new(
51
+ requirements: dependency.requirements,
52
+ latest_version: latest_version&.to_s,
53
+ tag_for_latest_version: tag_for_latest_version
54
+ ).updated_requirements
55
+ end
56
+
57
+ sig { returns(T::Boolean) }
58
+ def requirements_unlocked_or_can_be?
59
+ # If the requirement comes from a proxy URL then there's no way for
60
+ # us to update it
61
+ !proxy_requirement?
62
+ end
63
+
64
+ private
65
+
66
+ sig { override.returns(T::Boolean) }
67
+ def latest_version_resolvable_with_full_unlock?
68
+ # Full unlock checks aren't relevant for OpenTofu files
69
+ false
70
+ end
71
+
72
+ sig { override.returns(T::Array[Dependabot::Dependency]) }
73
+ def updated_dependencies_after_full_unlock
74
+ raise NotImplementedError
75
+ end
76
+
77
+ sig { returns(T.nilable(Dependabot::Opentofu::Version)) }
78
+ def latest_version_for_registry_dependency
79
+ return unless registry_dependency?
80
+
81
+ return @latest_version_for_registry_dependency if @latest_version_for_registry_dependency
82
+
83
+ versions = all_module_versions
84
+ # Filter versions which are in cooldown period
85
+ if cooldown_enabled? # rubocop:disable Style/IfUnlessModifier
86
+ versions = latest_version_resolver.filter_versions_in_cooldown_period_from_module(versions)
87
+ end
88
+ versions.reject!(&:prerelease?) unless wants_prerelease?
89
+ versions.reject! { |v| ignore_requirements.any? { |r| r.satisfied_by?(v) } }
90
+ @latest_version_for_registry_dependency = T.let(
91
+ versions.max,
92
+ T.nilable(Dependabot::Opentofu::Version)
93
+ )
94
+ end
95
+
96
+ sig { returns(T::Array[Dependabot::Opentofu::Version]) }
97
+ def all_module_versions
98
+ identifier = dependency_source_details&.fetch(:module_identifier)
99
+ registry_client.all_module_versions(identifier: identifier)
100
+ end
101
+
102
+ sig { returns(T::Array[Dependabot::Opentofu::Version]) }
103
+ def all_provider_versions
104
+ identifier = dependency_source_details&.fetch(:module_identifier)
105
+ registry_client.all_provider_versions(identifier: identifier)
106
+ end
107
+
108
+ sig { returns(Dependabot::Opentofu::RegistryClient) }
109
+ def registry_client
110
+ @registry_client ||= T.let(
111
+ begin
112
+ hostname = dependency_source_details&.fetch(:registry_hostname)
113
+ RegistryClient.new(hostname: hostname, credentials: credentials)
114
+ end,
115
+ T.nilable(Dependabot::Opentofu::RegistryClient)
116
+ )
117
+ end
118
+
119
+ sig { returns(T.nilable(Dependabot::Opentofu::Version)) }
120
+ def latest_version_for_provider_dependency
121
+ return unless provider_dependency?
122
+
123
+ return @latest_version_for_provider_dependency if @latest_version_for_provider_dependency
124
+
125
+ versions = all_provider_versions
126
+ # Filter versions which are in cooldown period
127
+ if cooldown_enabled?
128
+ versions = latest_version_resolver.filter_versions_in_cooldown_period_from_provider(versions)
129
+ end
130
+ versions.reject!(&:prerelease?) unless wants_prerelease?
131
+ versions.reject! { |v| ignore_requirements.any? { |r| r.satisfied_by?(v) } }
132
+
133
+ @latest_version_for_provider_dependency = T.let(
134
+ versions.max,
135
+ T.nilable(Dependabot::Opentofu::Version)
136
+ )
137
+ end
138
+
139
+ sig { returns(T::Boolean) }
140
+ def wants_prerelease?
141
+ current_version = dependency.version
142
+ if current_version &&
143
+ version_class.correct?(current_version) &&
144
+ version_class.new(current_version).prerelease?
145
+ return true
146
+ end
147
+
148
+ dependency.requirements.any? do |req|
149
+ req[:requirement]&.match?(/\d-[A-Za-z0-9]/)
150
+ end
151
+ end
152
+
153
+ sig { returns(T.nilable(T.any(Dependabot::Version, String))) }
154
+ def latest_version_for_git_dependency
155
+ # If the module isn't pinned then there's nothing for us to update
156
+ # (since there's no lockfile to update the version in). We still
157
+ # return the latest commit for the given branch, in order to keep
158
+ # this method consistent
159
+ return git_commit_checker.head_commit_for_current_branch unless git_commit_checker.pinned?
160
+
161
+ # If the dependency is pinned to a tag that looks like a version then
162
+ # we want to update that tag. Because we don't have a lockfile, the
163
+ # latest version is the tag itself.
164
+ if git_commit_checker.pinned_ref_looks_like_version?
165
+ # Filter version tags that are in cooldown period
166
+ latest_tag = latest_version_resolver.latest_version_tag&.fetch(:tag)
167
+ version_rgx = GitCommitChecker::VERSION_REGEX
168
+ return unless latest_tag.match(version_rgx)
169
+
170
+ version = latest_tag.match(version_rgx)
171
+ .named_captures.fetch("version")
172
+ return version_class.new(version)
173
+ end
174
+
175
+ # If the dependency is pinned to a tag that doesn't look like a
176
+ # version then there's nothing we can do.
177
+ nil
178
+ end
179
+
180
+ sig { returns(T.nilable(String)) }
181
+ def tag_for_latest_version
182
+ return unless git_commit_checker.git_dependency?
183
+ return unless git_commit_checker.pinned?
184
+ return unless git_commit_checker.pinned_ref_looks_like_version?
185
+
186
+ latest_tag = git_commit_checker.local_tag_for_latest_version
187
+ &.fetch(:tag)
188
+
189
+ version_rgx = GitCommitChecker::VERSION_REGEX
190
+ return unless latest_tag.match(version_rgx)
191
+
192
+ latest_tag
193
+ end
194
+
195
+ sig { returns(T::Boolean) }
196
+ def proxy_requirement?
197
+ dependency.requirements.any? do |req|
198
+ req.fetch(:source)&.fetch(:proxy_url, nil)
199
+ end
200
+ end
201
+
202
+ sig { returns(T::Boolean) }
203
+ def registry_dependency?
204
+ return false if dependency_source_details.nil?
205
+
206
+ dependency_source_details&.fetch(:type) == "registry"
207
+ end
208
+
209
+ sig { returns(T::Boolean) }
210
+ def provider_dependency?
211
+ return false if dependency_source_details.nil?
212
+
213
+ dependency_source_details&.fetch(:type) == "provider"
214
+ end
215
+
216
+ sig { returns(T.nilable(T::Hash[T.any(String, Symbol), T.untyped])) }
217
+ def dependency_source_details
218
+ dependency.source_details(allowed_types: ELIGIBLE_SOURCE_TYPES)
219
+ end
220
+
221
+ sig { returns(T::Boolean) }
222
+ def git_dependency?
223
+ git_commit_checker.git_dependency?
224
+ end
225
+
226
+ sig { returns(LatestVersionResolver) }
227
+ def latest_version_resolver
228
+ LatestVersionResolver.new(
229
+ dependency: dependency,
230
+ credentials: credentials,
231
+ cooldown_options: update_cooldown,
232
+ git_commit_checker: git_commit_checker
233
+ )
234
+ end
235
+
236
+ sig { returns(Dependabot::GitCommitChecker) }
237
+ def git_commit_checker
238
+ @git_commit_checker ||= T.let(
239
+ GitCommitChecker.new(
240
+ dependency: dependency,
241
+ credentials: credentials,
242
+ ignored_versions: ignored_versions,
243
+ raise_on_ignored: raise_on_ignored
244
+ ),
245
+ T.nilable(Dependabot::GitCommitChecker)
246
+ )
247
+ end
248
+
249
+ sig { returns(T::Boolean) }
250
+ def cooldown_enabled?
251
+ # This is a simple check to see if user has put cooldown days.
252
+ # If not set, then we aassume user does not want cooldown.
253
+ # Since OpenTofu does not support Semver versioning, So option left
254
+ # for the user is to set cooldown default days.
255
+ return false if update_cooldown.nil?
256
+
257
+ T.must(update_cooldown&.default_days).positive?
258
+ end
259
+ end
260
+ end
261
+ end
262
+
263
+ Dependabot::UpdateCheckers
264
+ .register("opentofu", Dependabot::Opentofu::UpdateChecker)