dependabot-github_actions 0.393.0 → 0.394.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.
@@ -0,0 +1,191 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "dependabot/github_actions/file_updater/workflow_updater"
5
+
6
+ module Dependabot
7
+ module GithubActions
8
+ class FileUpdater < Dependabot::FileUpdaters::Base
9
+ class WorkflowUpdater
10
+ class SourceCommentUpdater
11
+ extend T::Sig
12
+
13
+ sig do
14
+ params(
15
+ workflow_file: WorkflowFile,
16
+ commenter: VersionCommenter,
17
+ metadata_builder: WorkflowFile::MetadataBuilder,
18
+ comment_locator: SourceCommentLocator
19
+ ).void
20
+ end
21
+ def initialize(workflow_file:, commenter:, metadata_builder:, comment_locator:)
22
+ @workflow_file = workflow_file
23
+ @commenter = commenter
24
+ @metadata_builder = metadata_builder
25
+ @comment_locator = comment_locator
26
+ end
27
+
28
+ sig { params(update: SourceUpdate).returns(T::Array[ContentEdit]) }
29
+ def edits_for(update)
30
+ located_comment = located_comment_for(update)
31
+ if located_comment
32
+ recognized = commenter.recognized_comment?(
33
+ located_comment.comment,
34
+ update.old_ref,
35
+ update.new_ref
36
+ )
37
+ if located_comment.syntax_adjacent || recognized
38
+ return edits_for_existing_comment(update, located_comment, recognized)
39
+ end
40
+ end
41
+
42
+ edits_for_new_comment(update)
43
+ end
44
+
45
+ private
46
+
47
+ sig { returns(WorkflowFile) }
48
+ attr_reader :workflow_file
49
+
50
+ sig { returns(VersionCommenter) }
51
+ attr_reader :commenter
52
+
53
+ sig { returns(WorkflowFile::MetadataBuilder) }
54
+ attr_reader :metadata_builder
55
+
56
+ sig { returns(SourceCommentLocator) }
57
+ attr_reader :comment_locator
58
+
59
+ sig do
60
+ params(
61
+ update: SourceUpdate,
62
+ located_comment: SourceCommentLocator::LocatedComment,
63
+ recognized: T::Boolean
64
+ ).returns(T::Array[ContentEdit])
65
+ end
66
+ def edits_for_existing_comment(update, located_comment, recognized)
67
+ updated_comment = commenter.updated_comment(
68
+ located_comment.comment,
69
+ update.old_ref,
70
+ update.new_ref
71
+ )
72
+ mapping = colliding_flow_mapping(update)
73
+ if recognized && !located_comment.syntax_adjacent && mapping
74
+ return relocated_mapping_comment_edits(mapping, located_comment, updated_comment)
75
+ end
76
+
77
+ replacement = updated_comment || (recognized ? "" : nil)
78
+ return [] unless replacement
79
+
80
+ [
81
+ ContentEdit.new(
82
+ start_offset: located_comment.start_offset,
83
+ end_offset: located_comment.end_offset,
84
+ replacement: replacement
85
+ )
86
+ ]
87
+ end
88
+
89
+ sig { params(update: SourceUpdate).returns(T::Array[ContentEdit]) }
90
+ def edits_for_new_comment(update)
91
+ new_comment = commenter.new_comment(update.old_ref, update.new_ref)
92
+ return [] unless new_comment
93
+
94
+ mapping_edit = colliding_flow_mapping_comment_edit(update, new_comment)
95
+ return [mapping_edit] if mapping_edit
96
+
97
+ line = T.must(metadata_builder.comment_anchor(update.declaration)).first
98
+ line_end = workflow_file.line_end_offset(line)
99
+ [ContentEdit.new(start_offset: line_end, end_offset: line_end, replacement: new_comment)]
100
+ end
101
+
102
+ sig { params(update: SourceUpdate).returns(T.nilable(SourceCommentLocator::LocatedComment)) }
103
+ def located_comment_for(update)
104
+ declaration_comment = comment_locator.for_declaration(update.declaration)
105
+ if declaration_comment
106
+ recognized = commenter.recognized_comment?(
107
+ declaration_comment.comment,
108
+ update.old_ref,
109
+ update.new_ref
110
+ )
111
+ return declaration_comment if declaration_comment.syntax_adjacent || recognized
112
+ end
113
+
114
+ sequence = update.declaration.steps_sequence
115
+ return unless sequence
116
+
117
+ sequence_comment = comment_locator.for_sequence(sequence)
118
+ return unless sequence_comment
119
+
120
+ recognized = commenter.recognized_comment?(sequence_comment.comment, update.old_ref, update.new_ref)
121
+ return unless sequence_comment.syntax_adjacent || recognized
122
+
123
+ sequence_comment
124
+ end
125
+
126
+ sig do
127
+ params(
128
+ mapping: Psych::Nodes::Mapping,
129
+ located_comment: SourceCommentLocator::LocatedComment,
130
+ updated_comment: T.nilable(String)
131
+ ).returns(T::Array[ContentEdit])
132
+ end
133
+ def relocated_mapping_comment_edits(mapping, located_comment, updated_comment)
134
+ deletion = ContentEdit.new(
135
+ start_offset: located_comment.start_offset,
136
+ end_offset: located_comment.end_offset,
137
+ replacement: ""
138
+ )
139
+ return [deletion] unless updated_comment
140
+
141
+ end_offset = node_end_offset(mapping)
142
+ indentation = " " * workflow_file.node_start_column(mapping)
143
+ insertion = ContentEdit.new(
144
+ start_offset: end_offset,
145
+ end_offset: end_offset,
146
+ replacement: "#{updated_comment}#{workflow_file.newline}#{indentation}"
147
+ )
148
+ [insertion, deletion]
149
+ end
150
+
151
+ sig { params(update: SourceUpdate, comment: String).returns(T.nilable(ContentEdit)) }
152
+ def colliding_flow_mapping_comment_edit(update, comment)
153
+ mapping = colliding_flow_mapping(update)
154
+ return unless mapping
155
+
156
+ end_offset = node_end_offset(mapping)
157
+ indentation = " " * workflow_file.node_start_column(mapping)
158
+ ContentEdit.new(
159
+ start_offset: end_offset,
160
+ end_offset: end_offset,
161
+ replacement: "#{comment}#{workflow_file.newline}#{indentation}"
162
+ )
163
+ end
164
+
165
+ sig { params(update: SourceUpdate).returns(T.nilable(Psych::Nodes::Mapping)) }
166
+ def colliding_flow_mapping(update)
167
+ declaration = update.declaration
168
+ return if declaration.steps_sequence
169
+ return unless metadata_builder.comment_line_collision?(declaration)
170
+
171
+ mapping = declaration.mapping_node
172
+ return unless mapping
173
+
174
+ flow_style = T.cast(Psych::Nodes::Mapping::FLOW, Integer)
175
+ return unless workflow_file.mapping_node_style(mapping) == flow_style
176
+
177
+ mapping
178
+ end
179
+
180
+ sig { params(node: Psych::Nodes::Node).returns(Integer) }
181
+ def node_end_offset(node)
182
+ workflow_file.offset(
183
+ workflow_file.node_end_line(node),
184
+ workflow_file.node_end_column(node)
185
+ )
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,387 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "dependabot/github_actions/file_updater/workflow_updater"
5
+
6
+ module Dependabot
7
+ module GithubActions
8
+ class FileUpdater < Dependabot::FileUpdaters::Base
9
+ class WorkflowUpdater
10
+ class SourceUpdater
11
+ extend T::Sig
12
+
13
+ sig do
14
+ params(
15
+ workflow_file: WorkflowFile,
16
+ updates: T::Array[SourceUpdate],
17
+ commenter: VersionCommenter,
18
+ comment_finder: YamlCommentFinder
19
+ ).void
20
+ end
21
+ def initialize(workflow_file:, updates:, commenter:, comment_finder:)
22
+ @workflow_file = workflow_file
23
+ @updates = updates
24
+ @commenter = commenter
25
+ @metadata_builder = T.let(WorkflowFile::MetadataBuilder.new(workflow_file), WorkflowFile::MetadataBuilder)
26
+ @comment_locator = T.let(
27
+ SourceCommentLocator.new(
28
+ workflow_file: workflow_file,
29
+ comment_finder: comment_finder,
30
+ metadata_builder: @metadata_builder
31
+ ),
32
+ SourceCommentLocator
33
+ )
34
+ @comment_updater = T.let(
35
+ SourceCommentUpdater.new(
36
+ workflow_file: workflow_file,
37
+ commenter: commenter,
38
+ metadata_builder: @metadata_builder,
39
+ comment_locator: @comment_locator
40
+ ),
41
+ SourceCommentUpdater
42
+ )
43
+ end
44
+
45
+ sig { returns(String) }
46
+ def updated_content
47
+ return workflow_file.content if updates.empty?
48
+
49
+ sequence_updates = grouped_flow_sequence_updates
50
+ edits = sequence_updates.flat_map do |sequence, grouped_updates|
51
+ expanded_flow_sequence_edits(sequence, grouped_updates)
52
+ end
53
+
54
+ updates.each do |update|
55
+ sequence = expanded_sequence_for_update(sequence_updates, update)
56
+ source_node = T.must(update.declaration.source_node)
57
+ source_in_expanded_sequence = sequence_updates.keys.any? do |expanded_sequence|
58
+ node_within?(source_node, expanded_sequence)
59
+ end
60
+ edits << source_value_edit(update) unless source_in_expanded_sequence
61
+
62
+ next if sequence
63
+
64
+ edits.concat(comment_updater.edits_for(update))
65
+ end
66
+
67
+ apply_content_edits(edits)
68
+ end
69
+
70
+ private
71
+
72
+ sig { returns(WorkflowFile) }
73
+ attr_reader :workflow_file
74
+
75
+ sig { returns(T::Array[SourceUpdate]) }
76
+ attr_reader :updates
77
+
78
+ sig { returns(VersionCommenter) }
79
+ attr_reader :commenter
80
+
81
+ sig { returns(WorkflowFile::MetadataBuilder) }
82
+ attr_reader :metadata_builder
83
+
84
+ sig { returns(SourceCommentLocator) }
85
+ attr_reader :comment_locator
86
+
87
+ sig { returns(SourceCommentUpdater) }
88
+ attr_reader :comment_updater
89
+
90
+ sig { returns(T::Hash[Psych::Nodes::Sequence, T::Array[SourceUpdate]]) }
91
+ def grouped_flow_sequence_updates
92
+ sequences = updates.flat_map { |update| sequences_to_expand(update) }
93
+ groups = T.let({}, T::Hash[Psych::Nodes::Sequence, T::Array[SourceUpdate]])
94
+ sequences.uniq.each { |sequence| groups[sequence] = [] }
95
+
96
+ updates.each do |update|
97
+ sequence = update.declaration.steps_sequence
98
+ T.must(groups[sequence]) << update if sequence && groups.key?(sequence)
99
+ end
100
+ groups
101
+ end
102
+
103
+ sig { params(update: SourceUpdate).returns(T::Array[Psych::Nodes::Sequence]) }
104
+ def sequences_to_expand(update)
105
+ return [] unless commenter.sha?(update.new_ref)
106
+
107
+ sequences = metadata_builder.collision_sequences_for(update.declaration)
108
+ collision = metadata_builder.comment_line_collision?(update.declaration)
109
+ own_sequence = update.declaration.steps_sequence
110
+ sequence_comment = sequence_comment_requires_expansion?(update)
111
+ sequences << own_sequence if own_sequence && sequence_comment
112
+ return [] unless collision || sequence_comment
113
+
114
+ sequences.uniq.select do |sequence|
115
+ workflow_file.sequence_node_style(sequence) == Psych::Nodes::Sequence::FLOW
116
+ end
117
+ end
118
+
119
+ sig { params(update: SourceUpdate).returns(T::Boolean) }
120
+ def sequence_comment_requires_expansion?(update)
121
+ sequence = update.declaration.steps_sequence
122
+ return false unless sequence
123
+
124
+ located_comment = comment_locator.for_sequence(sequence)
125
+ return false unless located_comment
126
+
127
+ commenter.recognized_comment?(located_comment.comment, update.old_ref, update.new_ref)
128
+ end
129
+
130
+ sig do
131
+ params(
132
+ sequence_updates: T::Hash[Psych::Nodes::Sequence, T::Array[SourceUpdate]],
133
+ update: SourceUpdate
134
+ ).returns(T.nilable(Psych::Nodes::Sequence))
135
+ end
136
+ def expanded_sequence_for_update(sequence_updates, update)
137
+ sequence_updates.each do |sequence, grouped_updates|
138
+ return sequence if grouped_updates.include?(update)
139
+ end
140
+ nil
141
+ end
142
+
143
+ sig do
144
+ params(
145
+ sequence: Psych::Nodes::Sequence,
146
+ grouped_updates: T::Array[SourceUpdate]
147
+ ).returns(T::Array[ContentEdit])
148
+ end
149
+ def expanded_flow_sequence_edits(sequence, grouped_updates)
150
+ key_node = T.must(workflow_file.steps_key_node_for(sequence))
151
+ children = workflow_file.node_children(sequence)
152
+ item_indent = " " * (workflow_file.node_start_column(key_node) + 2)
153
+ closing_indent = " " * workflow_file.node_start_column(key_node)
154
+ sequence_comments = FlowSequenceComments.new(
155
+ workflow_file: workflow_file,
156
+ sequence: sequence,
157
+ commenter: commenter,
158
+ comment_locator: comment_locator,
159
+ item_indent: item_indent
160
+ )
161
+ trailing_comment_edit = sequence_comments.claim_trailing_comment(grouped_updates)
162
+
163
+ lines = sequence_comments.leading_lines + children.each_with_index.map do |child, index|
164
+ render_flow_sequence_child(
165
+ child,
166
+ index,
167
+ children.length,
168
+ grouped_updates,
169
+ sequence_comments
170
+ )
171
+ end
172
+
173
+ edits = [
174
+ ContentEdit.new(
175
+ start_offset: node_start_offset(sequence),
176
+ end_offset: node_end_offset(sequence),
177
+ replacement: flow_sequence_replacement(sequence, lines, closing_indent)
178
+ )
179
+ ]
180
+ edits << trailing_comment_edit if trailing_comment_edit
181
+ edits
182
+ end
183
+
184
+ sig do
185
+ params(
186
+ child: Psych::Nodes::Node,
187
+ index: Integer,
188
+ item_count: Integer,
189
+ grouped_updates: T::Array[SourceUpdate],
190
+ sequence_comments: FlowSequenceComments
191
+ ).returns(String)
192
+ end
193
+ def render_flow_sequence_child(
194
+ child,
195
+ index,
196
+ item_count,
197
+ grouped_updates,
198
+ sequence_comments
199
+ )
200
+ source_updates = updates.select do |update|
201
+ source_node = T.must(update.declaration.source_node)
202
+ node_within?(source_node, child)
203
+ end
204
+ source_updates = source_updates.uniq { |update| T.must(update.declaration.source_node) }
205
+ comment_updates = grouped_updates.select do |update|
206
+ update.declaration.sequence_item_index == index
207
+ end
208
+ child_source = workflow_file.node_source(child).strip
209
+ child_source = replace_declarations_in_node_source(child, child_source, source_updates)
210
+
211
+ sequence_comments.render_item(
212
+ index: index,
213
+ item_count: item_count,
214
+ source: child_source,
215
+ updates: comment_updates
216
+ )
217
+ end
218
+
219
+ sig do
220
+ params(
221
+ sequence: Psych::Nodes::Sequence,
222
+ lines: T::Array[String],
223
+ closing_indent: String
224
+ ).returns(String)
225
+ end
226
+ def flow_sequence_replacement(sequence, lines, closing_indent)
227
+ source = workflow_file.node_source(sequence)
228
+ opening_bracket = T.must(source.b.index("["))
229
+ closing_bracket = T.must(source.b.rindex("]"))
230
+ prefix = source.byteslice(0...opening_bracket) || ""
231
+ suffix = source.byteslice((closing_bracket + 1)..) || ""
232
+ expanded = [
233
+ "[",
234
+ lines.join(workflow_file.newline),
235
+ "#{closing_indent}]"
236
+ ].join(workflow_file.newline)
237
+ prefix + expanded + suffix
238
+ end
239
+
240
+ sig do
241
+ params(
242
+ container_node: Psych::Nodes::Node,
243
+ container_source: String,
244
+ source_updates: T::Array[SourceUpdate]
245
+ ).returns(String)
246
+ end
247
+ def replace_declarations_in_node_source(container_node, container_source, source_updates)
248
+ container_start = node_start_offset(container_node)
249
+ changes = unique_edits(source_updates.map { |update| source_value_change(update) })
250
+ changes.sort_by(&:start_offset).reverse_each.reduce(container_source) do |source, change|
251
+ replace_bytes(
252
+ source,
253
+ change.start_offset - container_start,
254
+ change.end_offset - container_start,
255
+ change.replacement
256
+ )
257
+ end
258
+ end
259
+
260
+ sig { params(update: SourceUpdate).returns(ContentEdit) }
261
+ def source_value_edit(update)
262
+ source_value_change(update)
263
+ end
264
+
265
+ sig { params(update: SourceUpdate).returns(ContentEdit) }
266
+ def source_value_change(update)
267
+ source_node = T.must(update.declaration.source_node)
268
+ node_start = node_start_offset(source_node)
269
+ source = workflow_file.node_source(source_node)
270
+ declaration = update.old_declaration.strip
271
+ relative_start = source.b.index(declaration.b)
272
+ if relative_start
273
+ return ContentEdit.new(
274
+ start_offset: node_start + relative_start,
275
+ end_offset: node_start + relative_start + declaration.bytesize,
276
+ replacement: update.new_declaration.strip
277
+ )
278
+ end
279
+
280
+ quoted_scalar_change(source_node, source, node_start, update)
281
+ end
282
+
283
+ sig do
284
+ params(
285
+ source_node: Psych::Nodes::Node,
286
+ source: String,
287
+ node_start: Integer,
288
+ update: SourceUpdate
289
+ ).returns(ContentEdit)
290
+ end
291
+ def quoted_scalar_change(source_node, source, node_start, update)
292
+ raise "Expected workflow source to include #{update.old_declaration}" unless
293
+ source_node.is_a?(Psych::Nodes::Scalar)
294
+
295
+ style = workflow_file.scalar_node_style(source_node)
296
+ quote = case style
297
+ when Psych::Nodes::Scalar::SINGLE_QUOTED then "'"
298
+ when Psych::Nodes::Scalar::DOUBLE_QUOTED then '"'
299
+ end
300
+ raise "Expected workflow source to include #{update.old_declaration}" unless quote
301
+
302
+ value_start = T.must(source.b.index(quote.b))
303
+ value_end = T.must(source.b.rindex(quote.b))
304
+ replacement = escaped_quoted_value(update.new_declaration.strip, quote)
305
+ ContentEdit.new(
306
+ start_offset: node_start + value_start + 1,
307
+ end_offset: node_start + value_end,
308
+ replacement: replacement
309
+ )
310
+ end
311
+
312
+ sig { params(value: String, quote: String).returns(String) }
313
+ def escaped_quoted_value(value, quote)
314
+ return value.gsub("'", "''") if quote == "'"
315
+
316
+ value.gsub("\\", "\\\\").gsub('"', '\\"')
317
+ end
318
+
319
+ sig { params(child: Psych::Nodes::Node, parent: Psych::Nodes::Node).returns(T::Boolean) }
320
+ def node_within?(child, parent)
321
+ node_start_offset(child) >= node_start_offset(parent) &&
322
+ node_end_offset(child) <= node_end_offset(parent)
323
+ end
324
+
325
+ sig { params(node: Psych::Nodes::Node).returns(Integer) }
326
+ def node_start_offset(node)
327
+ workflow_file.offset(
328
+ workflow_file.node_start_line(node),
329
+ workflow_file.node_start_column(node)
330
+ )
331
+ end
332
+
333
+ sig { params(node: Psych::Nodes::Node).returns(Integer) }
334
+ def node_end_offset(node)
335
+ workflow_file.offset(
336
+ workflow_file.node_end_line(node),
337
+ workflow_file.node_end_column(node)
338
+ )
339
+ end
340
+
341
+ sig { params(edits: T::Array[ContentEdit]).returns(String) }
342
+ def apply_content_edits(edits)
343
+ ordered_edits = unique_edits(edits).sort_by(&:start_offset)
344
+ (0...(ordered_edits.length - 1)).each do |index|
345
+ left = T.must(ordered_edits[index])
346
+ right = T.must(ordered_edits[index + 1])
347
+ raise "Overlapping workflow edits" if left.end_offset > right.start_offset
348
+ end
349
+
350
+ ordered_edits.reverse_each.reduce(workflow_file.content) do |content, edit|
351
+ replace_bytes(content, edit.start_offset, edit.end_offset, edit.replacement)
352
+ end
353
+ end
354
+
355
+ sig { params(edits: T::Array[ContentEdit]).returns(T::Array[ContentEdit]) }
356
+ def unique_edits(edits)
357
+ edits_by_range = T.let({}, T::Hash[[Integer, Integer], ContentEdit])
358
+ edits.each do |edit|
359
+ key = [edit.start_offset, edit.end_offset]
360
+ existing = edits_by_range[key]
361
+ if existing && existing.replacement != edit.replacement
362
+ raise "Conflicting workflow edits at #{edit.start_offset}"
363
+ end
364
+
365
+ edits_by_range[key] = edit
366
+ end
367
+ edits_by_range.values
368
+ end
369
+
370
+ sig do
371
+ params(
372
+ content: String,
373
+ start_offset: Integer,
374
+ end_offset: Integer,
375
+ replacement: String
376
+ ).returns(String)
377
+ end
378
+ def replace_bytes(content, start_offset, end_offset, replacement)
379
+ prefix = content.byteslice(0...start_offset) || ""
380
+ suffix = content.byteslice(end_offset..) || ""
381
+ prefix + replacement + suffix
382
+ end
383
+ end
384
+ end
385
+ end
386
+ end
387
+ end
@@ -0,0 +1,117 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "dependabot/github_actions/file_updater/workflow_updater"
5
+
6
+ module Dependabot
7
+ module GithubActions
8
+ class FileUpdater < Dependabot::FileUpdaters::Base
9
+ class WorkflowUpdater
10
+ class VersionCommenter
11
+ extend T::Sig
12
+
13
+ sig do
14
+ params(
15
+ dependency: Dependabot::Dependency,
16
+ credentials: T::Array[Dependabot::Credential]
17
+ ).void
18
+ end
19
+ def initialize(dependency:, credentials:)
20
+ @dependency = dependency
21
+ @credentials = credentials
22
+ end
23
+
24
+ sig { params(comment: String, old_ref: String, new_ref: String).returns(T.nilable(String)) }
25
+ def updated_comment(comment, old_ref, new_ref)
26
+ comment = comment.rstrip
27
+ previous_version = previous_version_from_comment(comment, old_ref, new_ref)
28
+ return unless previous_version
29
+
30
+ new_version_tag = git_checker.most_specific_version_tag_for_sha(new_ref)
31
+ return unless new_version_tag
32
+
33
+ new_version = version_class.new(new_version_tag).to_s
34
+ comment.gsub(previous_version, new_version)
35
+ end
36
+
37
+ sig { params(comment: String, old_ref: String, new_ref: String).returns(T::Boolean) }
38
+ def recognized_comment?(comment, old_ref, new_ref)
39
+ !previous_version_from_comment(comment.rstrip, old_ref, new_ref).nil?
40
+ end
41
+
42
+ sig { params(old_ref: String, new_ref: String).returns(T.nilable(String)) }
43
+ def new_comment(old_ref, new_ref)
44
+ return unless tag_to_sha?(old_ref, new_ref)
45
+
46
+ comment_for_ref(new_ref)
47
+ end
48
+
49
+ sig { params(ref: String).returns(T.nilable(String)) }
50
+ def comment_for_ref(ref)
51
+ return unless sha?(ref)
52
+
53
+ version_tag = git_checker.most_specific_version_tag_for_sha(ref)
54
+ return unless version_tag
55
+
56
+ " # #{version_tag}"
57
+ end
58
+
59
+ sig { params(ref: String).returns(T::Boolean) }
60
+ def sha?(ref)
61
+ git_checker.ref_looks_like_commit_sha?(ref)
62
+ end
63
+
64
+ private
65
+
66
+ sig { returns(Dependabot::Dependency) }
67
+ attr_reader :dependency
68
+
69
+ sig { returns(T::Array[Dependabot::Credential]) }
70
+ attr_reader :credentials
71
+
72
+ sig { params(comment: String, old_ref: String, new_ref: String).returns(T.nilable(String)) }
73
+ def previous_version_from_comment(comment, old_ref, new_ref)
74
+ version_tags = if sha?(old_ref)
75
+ git_checker.most_specific_version_tags_for_sha(old_ref)
76
+ elsif tag_to_sha?(old_ref, new_ref)
77
+ version_tags_for_ref(old_ref)
78
+ else
79
+ []
80
+ end
81
+
82
+ version_tags
83
+ .filter_map { |tag| version_class.new(tag).to_s if version_class.correct?(tag) }
84
+ .select { |version| comment.end_with?(version) }
85
+ .max_by(&:length)
86
+ end
87
+
88
+ sig { params(ref: String).returns(T::Array[String]) }
89
+ def version_tags_for_ref(ref)
90
+ tags = [ref]
91
+ commit_sha = git_checker.head_commit_for_local_branch(ref)
92
+ tags.concat(git_checker.most_specific_version_tags_for_sha(commit_sha)) if commit_sha
93
+ tags.uniq
94
+ end
95
+
96
+ sig { params(old_ref: String, new_ref: String).returns(T::Boolean) }
97
+ def tag_to_sha?(old_ref, new_ref)
98
+ version_class.correct?(old_ref) && sha?(new_ref)
99
+ end
100
+
101
+ sig { returns(Dependabot::GitCommitChecker) }
102
+ def git_checker
103
+ @git_checker ||= T.let(
104
+ Dependabot::GitCommitChecker.new(dependency: dependency, credentials: credentials),
105
+ T.nilable(Dependabot::GitCommitChecker)
106
+ )
107
+ end
108
+
109
+ sig { returns(T.class_of(Dependabot::GithubActions::Version)) }
110
+ def version_class
111
+ GithubActions::Version
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end