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.
- checksums.yaml +4 -4
- data/lib/dependabot/github_actions/file_parser.rb +101 -77
- data/lib/dependabot/github_actions/file_updater/workflow_updater/flow_sequence_comments.rb +244 -0
- data/lib/dependabot/github_actions/file_updater/workflow_updater/source_comment_locator.rb +147 -0
- data/lib/dependabot/github_actions/file_updater/workflow_updater/source_comment_updater.rb +191 -0
- data/lib/dependabot/github_actions/file_updater/workflow_updater/source_updater.rb +387 -0
- data/lib/dependabot/github_actions/file_updater/workflow_updater/version_commenter.rb +117 -0
- data/lib/dependabot/github_actions/file_updater/workflow_updater/yaml_comment_finder.rb +96 -0
- data/lib/dependabot/github_actions/file_updater/workflow_updater.rb +222 -0
- data/lib/dependabot/github_actions/file_updater.rb +19 -77
- data/lib/dependabot/github_actions/update_checker.rb +56 -7
- data/lib/dependabot/github_actions/workflow_file/metadata_builder.rb +212 -0
- data/lib/dependabot/github_actions/workflow_file/node_context.rb +64 -0
- data/lib/dependabot/github_actions/workflow_file/uses_collector.rb +78 -0
- data/lib/dependabot/github_actions/workflow_file/uses_declaration.rb +76 -0
- data/lib/dependabot/github_actions/workflow_file.rb +414 -0
- metadata +18 -6
|
@@ -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
|