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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 21ec9924cfc31066fa2dccd2cf7ca50e9f7969efde8940d67adbd19443a2f46d
|
|
4
|
+
data.tar.gz: e98917f3e0def956dc94d9341c2a17a4c9739a517ebaec569b3bc766ba0a3014
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f16959b6407974932411f006a734f63cdae7279624a63ffe8a956521535688d030dce0b1ec588f9c611060060377f4dad024599d617257e5be386eeaf1154919
|
|
7
|
+
data.tar.gz: 647815b9aae2a57c32f60edf7c7c03e8095227fe561769beeb9c96ed7ff97ef6d2ef905d112e377a181f797ba9a06f06a793acb03cf8830e29cd896a0c1e3704
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# typed:
|
|
1
|
+
# typed: strong
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require "sorbet-runtime"
|
|
@@ -11,6 +11,7 @@ require "dependabot/file_parsers/base"
|
|
|
11
11
|
require "dependabot/github_actions/constants"
|
|
12
12
|
require "dependabot/github_actions/version"
|
|
13
13
|
require "dependabot/github_actions/package_manager"
|
|
14
|
+
require "dependabot/github_actions/workflow_file"
|
|
14
15
|
|
|
15
16
|
# For docs, see
|
|
16
17
|
# https://help.github.com/en/articles/configuring-a-workflow#referencing-actions-in-your-workflow
|
|
@@ -54,62 +55,106 @@ module Dependabot
|
|
|
54
55
|
sig { params(file: Dependabot::DependencyFile).returns(Dependabot::FileParsers::Base::DependencySet) }
|
|
55
56
|
def workfile_file_dependencies(file)
|
|
56
57
|
dependency_set = DependencySet.new
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
uses_strings = deep_fetch_uses(json.fetch("jobs", json.fetch("runs", nil))).uniq
|
|
62
|
-
|
|
63
|
-
uses_strings.each do |string|
|
|
64
|
-
# TODO: Support Docker references and path references
|
|
65
|
-
next if string.start_with?(".", "docker://")
|
|
66
|
-
next unless string.match?(GITHUB_REPO_REFERENCE)
|
|
67
|
-
|
|
68
|
-
dep = build_github_dependency(file, string)
|
|
69
|
-
git_checker = Dependabot::GitCommitChecker.new(
|
|
70
|
-
dependency: dep,
|
|
71
|
-
credentials: credentials,
|
|
72
|
-
consider_version_branches_pinned: true
|
|
73
|
-
)
|
|
74
|
-
if git_checker.git_repo_reachable?
|
|
75
|
-
next unless git_checker.pinned?
|
|
76
|
-
|
|
77
|
-
# If dep does not have an assigned (semver) version, look for a commit that references a semver tag
|
|
78
|
-
unless dep.version
|
|
79
|
-
resolved = git_checker.version_for_pinned_sha
|
|
80
|
-
|
|
81
|
-
if resolved
|
|
82
|
-
dep = Dependency.new(
|
|
83
|
-
name: dep.name,
|
|
84
|
-
version: resolved.to_s,
|
|
85
|
-
requirements: dep.requirements,
|
|
86
|
-
package_manager: dep.package_manager
|
|
87
|
-
)
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
end
|
|
58
|
+
workflow_file = WorkflowFile.new(T.must(file.content))
|
|
59
|
+
workflow_file.uses_declarations.group_by(&:value).each do |string, declarations|
|
|
60
|
+
dep = dependency_for_declarations(file, workflow_file, string, declarations)
|
|
61
|
+
next unless dep
|
|
91
62
|
|
|
92
63
|
dependency_set << dep
|
|
93
64
|
end
|
|
94
|
-
|
|
95
65
|
dependency_set
|
|
96
66
|
rescue Psych::SyntaxError, Psych::DisallowedClass, Psych::BadAlias
|
|
97
67
|
raise Dependabot::DependencyFileNotParseable, file.path
|
|
98
68
|
end
|
|
99
69
|
|
|
100
|
-
sig
|
|
101
|
-
|
|
70
|
+
sig do
|
|
71
|
+
params(
|
|
72
|
+
file: Dependabot::DependencyFile,
|
|
73
|
+
workflow_file: WorkflowFile,
|
|
74
|
+
string: String,
|
|
75
|
+
declarations: T::Array[WorkflowFile::UsesDeclaration]
|
|
76
|
+
).returns(T.nilable(Dependabot::Dependency))
|
|
77
|
+
end
|
|
78
|
+
def dependency_for_declarations(file, workflow_file, string, declarations)
|
|
79
|
+
# TODO: Support Docker references and path references
|
|
80
|
+
return if string.start_with?(".", "docker://")
|
|
81
|
+
return unless string.match?(GITHUB_REPO_REFERENCE)
|
|
82
|
+
|
|
83
|
+
metadata = declarations.map do |declaration|
|
|
84
|
+
declaration_metadata(workflow_file, declaration)
|
|
85
|
+
end
|
|
86
|
+
dep = build_github_dependency(file, string, metadata)
|
|
87
|
+
git_checker = Dependabot::GitCommitChecker.new(
|
|
88
|
+
dependency: dep,
|
|
89
|
+
credentials: credentials,
|
|
90
|
+
consider_version_branches_pinned: true
|
|
91
|
+
)
|
|
92
|
+
return dep unless git_checker.git_repo_reachable?
|
|
93
|
+
return unless git_checker.pinned?
|
|
94
|
+
|
|
95
|
+
dependency_with_resolved_version(dep, git_checker) || dep
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
sig do
|
|
99
|
+
params(
|
|
100
|
+
workflow_file: WorkflowFile,
|
|
101
|
+
declaration: WorkflowFile::UsesDeclaration
|
|
102
|
+
).returns(T::Hash[Symbol, Object])
|
|
103
|
+
end
|
|
104
|
+
def declaration_metadata(workflow_file, declaration)
|
|
105
|
+
metadata = T.let({ declaration_string: declaration.value }, T::Hash[Symbol, Object])
|
|
106
|
+
if workflow_file.source_metadata_required?(declaration)
|
|
107
|
+
metadata[:yaml_source] = workflow_file.source_metadata(declaration)
|
|
108
|
+
end
|
|
109
|
+
metadata
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
sig do
|
|
113
|
+
params(
|
|
114
|
+
dep: Dependabot::Dependency,
|
|
115
|
+
git_checker: Dependabot::GitCommitChecker
|
|
116
|
+
).returns(T.nilable(Dependabot::Dependency))
|
|
117
|
+
end
|
|
118
|
+
def dependency_with_resolved_version(dep, git_checker)
|
|
119
|
+
return if dep.version
|
|
120
|
+
|
|
121
|
+
resolved = git_checker.version_for_pinned_sha
|
|
122
|
+
return unless resolved
|
|
123
|
+
|
|
124
|
+
Dependency.new(
|
|
125
|
+
name: dep.name,
|
|
126
|
+
version: resolved.to_s,
|
|
127
|
+
requirements: dep.requirements,
|
|
128
|
+
package_manager: dep.package_manager
|
|
129
|
+
)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
sig do
|
|
133
|
+
params(
|
|
134
|
+
file: Dependabot::DependencyFile,
|
|
135
|
+
string: String,
|
|
136
|
+
metadata: T::Array[T::Hash[Symbol, Object]]
|
|
137
|
+
).returns(Dependabot::Dependency)
|
|
138
|
+
end
|
|
139
|
+
def build_github_dependency(file, string, metadata)
|
|
102
140
|
unless source&.hostname == GITHUB_COM
|
|
103
|
-
dep = github_dependency(file, string, T.must(source).hostname)
|
|
141
|
+
dep = github_dependency(file, string, T.must(source).hostname, metadata)
|
|
104
142
|
git_checker = Dependabot::GitCommitChecker.new(dependency: dep, credentials: credentials)
|
|
105
143
|
return dep if git_checker.git_repo_reachable?
|
|
106
144
|
end
|
|
107
145
|
|
|
108
|
-
github_dependency(file, string, GITHUB_COM)
|
|
146
|
+
github_dependency(file, string, GITHUB_COM, metadata)
|
|
109
147
|
end
|
|
110
148
|
|
|
111
|
-
sig
|
|
112
|
-
|
|
149
|
+
sig do
|
|
150
|
+
params(
|
|
151
|
+
file: Dependabot::DependencyFile,
|
|
152
|
+
string: String,
|
|
153
|
+
hostname: String,
|
|
154
|
+
metadata: T::Array[T::Hash[Symbol, Object]]
|
|
155
|
+
).returns(Dependabot::Dependency)
|
|
156
|
+
end
|
|
157
|
+
def github_dependency(file, string, hostname, metadata)
|
|
113
158
|
details = T.must(string.match(GITHUB_REPO_REFERENCE)).named_captures
|
|
114
159
|
repo_name = "#{details.fetch(OWNER_KEY)}/#{details.fetch(REPO_KEY)}"
|
|
115
160
|
path = details[PATH_KEY]
|
|
@@ -130,45 +175,24 @@ module Dependabot
|
|
|
130
175
|
Dependency.new(
|
|
131
176
|
name: name,
|
|
132
177
|
version: version,
|
|
133
|
-
requirements:
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
178
|
+
requirements: metadata.map do |details|
|
|
179
|
+
{
|
|
180
|
+
requirement: nil,
|
|
181
|
+
groups: [],
|
|
182
|
+
source: {
|
|
183
|
+
type: "git",
|
|
184
|
+
url: "https://#{hostname}/#{repo_name}".downcase,
|
|
185
|
+
ref: ref,
|
|
186
|
+
branch: nil
|
|
187
|
+
},
|
|
188
|
+
file: file.name,
|
|
189
|
+
metadata: details
|
|
190
|
+
}
|
|
191
|
+
end,
|
|
145
192
|
package_manager: PackageManager::NAME
|
|
146
193
|
)
|
|
147
194
|
end
|
|
148
195
|
|
|
149
|
-
sig { params(json_obj: T.untyped, found_uses: T::Array[String]).returns(T::Array[String]) }
|
|
150
|
-
def deep_fetch_uses(json_obj, found_uses = [])
|
|
151
|
-
case json_obj
|
|
152
|
-
when Hash then deep_fetch_uses_from_hash(json_obj, found_uses)
|
|
153
|
-
when Array then json_obj.flat_map { |o| deep_fetch_uses(o, found_uses) }
|
|
154
|
-
else []
|
|
155
|
-
end
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
sig { params(json_object: T::Hash[String, T.untyped], found_uses: T::Array[String]).returns(T::Array[String]) }
|
|
159
|
-
def deep_fetch_uses_from_hash(json_object, found_uses)
|
|
160
|
-
if json_object.key?(USES_KEY)
|
|
161
|
-
found_uses << json_object[USES_KEY]
|
|
162
|
-
elsif json_object.key?(STEPS_KEY)
|
|
163
|
-
# Bypass other fields as uses are under steps if they exist
|
|
164
|
-
deep_fetch_uses(json_object[STEPS_KEY], found_uses)
|
|
165
|
-
else
|
|
166
|
-
json_object.values.flat_map { |obj| deep_fetch_uses(obj, found_uses) }
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
found_uses
|
|
170
|
-
end
|
|
171
|
-
|
|
172
196
|
sig { returns(T::Array[Dependabot::DependencyFile]) }
|
|
173
197
|
def workflow_files
|
|
174
198
|
dependency_files.reject { |file| file.path.delete_prefix("/") == LOCKFILE_PATH }
|
|
@@ -0,0 +1,244 @@
|
|
|
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 FlowSequenceComments
|
|
11
|
+
extend T::Sig
|
|
12
|
+
|
|
13
|
+
class ItemComments < T::Struct
|
|
14
|
+
extend T::Sig
|
|
15
|
+
|
|
16
|
+
const :trailing, T.nilable(String)
|
|
17
|
+
const :standalone, T::Array[String]
|
|
18
|
+
|
|
19
|
+
sig { returns(T::Boolean) }
|
|
20
|
+
def any?
|
|
21
|
+
!trailing.nil? || standalone.any?
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
sig do
|
|
26
|
+
params(
|
|
27
|
+
workflow_file: WorkflowFile,
|
|
28
|
+
sequence: Psych::Nodes::Sequence,
|
|
29
|
+
commenter: VersionCommenter,
|
|
30
|
+
comment_locator: SourceCommentLocator,
|
|
31
|
+
item_indent: String
|
|
32
|
+
).void
|
|
33
|
+
end
|
|
34
|
+
def initialize(workflow_file:, sequence:, commenter:, comment_locator:, item_indent:)
|
|
35
|
+
@workflow_file = workflow_file
|
|
36
|
+
@sequence = sequence
|
|
37
|
+
@commenter = commenter
|
|
38
|
+
@comment_locator = comment_locator
|
|
39
|
+
@item_indent = item_indent
|
|
40
|
+
@leading_comments = T.let(build_leading_comments, T::Array[String])
|
|
41
|
+
@source_comments = T.let(build_source_comments, T::Hash[Integer, ItemComments])
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
sig { params(index: Integer, updates: T::Array[SourceUpdate]).returns(ItemComments) }
|
|
45
|
+
def rendered_for(index, updates)
|
|
46
|
+
source_comments = @source_comments.fetch(index, ItemComments.new(trailing: nil, standalone: []))
|
|
47
|
+
return generated_comments(updates) unless source_comments.any?
|
|
48
|
+
|
|
49
|
+
ItemComments.new(
|
|
50
|
+
trailing: process_comment(source_comments.trailing, updates, trailing: true),
|
|
51
|
+
standalone: source_comments.standalone.filter_map do |comment|
|
|
52
|
+
process_comment(comment, updates, trailing: false)
|
|
53
|
+
end
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
sig do
|
|
58
|
+
params(
|
|
59
|
+
index: Integer,
|
|
60
|
+
item_count: Integer,
|
|
61
|
+
source: String,
|
|
62
|
+
updates: T::Array[SourceUpdate]
|
|
63
|
+
).returns(String)
|
|
64
|
+
end
|
|
65
|
+
def render_item(index:, item_count:, source:, updates:)
|
|
66
|
+
comments = rendered_for(index, updates)
|
|
67
|
+
comma = index == item_count - 1 ? "" : ","
|
|
68
|
+
line = "#{item_indent}#{source}#{comma}#{comments.trailing}"
|
|
69
|
+
standalone = comments.standalone.map { |comment| "#{item_indent}#{comment}" }
|
|
70
|
+
([line] + standalone).join(workflow_file.newline)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
sig { returns(T::Array[String]) }
|
|
74
|
+
def leading_lines
|
|
75
|
+
@leading_comments.map { |comment| "#{item_indent}#{comment}" }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
sig { params(updates: T::Array[SourceUpdate]).returns(T.nilable(ContentEdit)) }
|
|
79
|
+
def claim_trailing_comment(updates)
|
|
80
|
+
located_comment = comment_locator.for_sequence(sequence)
|
|
81
|
+
return unless located_comment
|
|
82
|
+
|
|
83
|
+
update = updates.find do |candidate|
|
|
84
|
+
commenter.recognized_comment?(
|
|
85
|
+
located_comment.comment,
|
|
86
|
+
candidate.old_ref,
|
|
87
|
+
candidate.new_ref
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
return unless update
|
|
91
|
+
return unless update.declaration.sequence_item_index
|
|
92
|
+
|
|
93
|
+
add_source_comment(T.must(update.declaration.sequence_item_index), located_comment.comment)
|
|
94
|
+
ContentEdit.new(
|
|
95
|
+
start_offset: located_comment.start_offset,
|
|
96
|
+
end_offset: located_comment.end_offset,
|
|
97
|
+
replacement: ""
|
|
98
|
+
)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
sig { returns(WorkflowFile) }
|
|
104
|
+
attr_reader :workflow_file
|
|
105
|
+
|
|
106
|
+
sig { returns(Psych::Nodes::Sequence) }
|
|
107
|
+
attr_reader :sequence
|
|
108
|
+
|
|
109
|
+
sig { returns(VersionCommenter) }
|
|
110
|
+
attr_reader :commenter
|
|
111
|
+
|
|
112
|
+
sig { returns(SourceCommentLocator) }
|
|
113
|
+
attr_reader :comment_locator
|
|
114
|
+
|
|
115
|
+
sig { returns(String) }
|
|
116
|
+
attr_reader :item_indent
|
|
117
|
+
|
|
118
|
+
sig { returns(T::Hash[Integer, ItemComments]) }
|
|
119
|
+
def build_source_comments
|
|
120
|
+
children = workflow_file.node_children(sequence)
|
|
121
|
+
closing_offset = closing_bracket_offset
|
|
122
|
+
|
|
123
|
+
children.each_with_index.to_h do |child, index|
|
|
124
|
+
gap_end = children[index + 1] ? node_start_offset(T.must(children[index + 1])) : closing_offset
|
|
125
|
+
gap = workflow_file.content.byteslice(node_end_offset(child)...gap_end) || ""
|
|
126
|
+
[index, comments_from_gap(gap)]
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
sig { returns(T::Array[String]) }
|
|
131
|
+
def build_leading_comments
|
|
132
|
+
first_child = workflow_file.node_children(sequence).first
|
|
133
|
+
return [] unless first_child
|
|
134
|
+
|
|
135
|
+
gap = workflow_file.content.byteslice((opening_bracket_offset + 1)...node_start_offset(first_child)) || ""
|
|
136
|
+
gap.lines.filter_map { |line| find_comment(line, trailing: false) }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
sig { params(gap: String).returns(ItemComments) }
|
|
140
|
+
def comments_from_gap(gap)
|
|
141
|
+
lines = gap.lines
|
|
142
|
+
first_line = lines.shift || gap
|
|
143
|
+
trailing = find_comment(first_line, trailing: true)
|
|
144
|
+
standalone = lines.filter_map { |line| find_comment(line, trailing: false) }
|
|
145
|
+
|
|
146
|
+
ItemComments.new(trailing: trailing, standalone: standalone)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
sig { params(line: String, trailing: T::Boolean).returns(T.nilable(String)) }
|
|
150
|
+
def find_comment(line, trailing:)
|
|
151
|
+
body = line.delete_suffix("\n").delete_suffix("\r")
|
|
152
|
+
comment = comment_locator.find_line(body)
|
|
153
|
+
return unless comment
|
|
154
|
+
|
|
155
|
+
trailing ? " #{comment.lstrip}" : comment.lstrip
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
sig { params(index: Integer, comment: String).void }
|
|
159
|
+
def add_source_comment(index, comment)
|
|
160
|
+
existing = @source_comments.fetch(index, ItemComments.new(trailing: nil, standalone: []))
|
|
161
|
+
@source_comments[index] =
|
|
162
|
+
if existing.trailing
|
|
163
|
+
ItemComments.new(
|
|
164
|
+
trailing: existing.trailing,
|
|
165
|
+
standalone: existing.standalone + [comment.lstrip]
|
|
166
|
+
)
|
|
167
|
+
else
|
|
168
|
+
ItemComments.new(
|
|
169
|
+
trailing: " #{comment.lstrip}",
|
|
170
|
+
standalone: existing.standalone
|
|
171
|
+
)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
sig do
|
|
176
|
+
params(
|
|
177
|
+
comment: T.nilable(String),
|
|
178
|
+
updates: T::Array[SourceUpdate],
|
|
179
|
+
trailing: T::Boolean
|
|
180
|
+
).returns(T.nilable(String))
|
|
181
|
+
end
|
|
182
|
+
def process_comment(comment, updates, trailing:)
|
|
183
|
+
return unless comment
|
|
184
|
+
|
|
185
|
+
index = 0
|
|
186
|
+
while index < updates.length
|
|
187
|
+
update = T.must(updates[index])
|
|
188
|
+
updated = commenter.updated_comment(comment, update.old_ref, update.new_ref)
|
|
189
|
+
return normalize_comment(updated, trailing: trailing) if updated
|
|
190
|
+
return if commenter.recognized_comment?(comment, update.old_ref, update.new_ref)
|
|
191
|
+
|
|
192
|
+
index += 1
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
normalize_comment(comment, trailing: trailing)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
sig { params(comment: String, trailing: T::Boolean).returns(String) }
|
|
199
|
+
def normalize_comment(comment, trailing:)
|
|
200
|
+
trailing ? " #{comment.lstrip}" : comment.lstrip
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
sig { params(updates: T::Array[SourceUpdate]).returns(ItemComments) }
|
|
204
|
+
def generated_comments(updates)
|
|
205
|
+
comments = updates.filter_map { |update| commenter.new_comment(update.old_ref, update.new_ref) }.uniq
|
|
206
|
+
raise "Conflicting version comments for one workflow step" if comments.length > 1
|
|
207
|
+
|
|
208
|
+
ItemComments.new(trailing: comments.first, standalone: [])
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
sig { returns(Integer) }
|
|
212
|
+
def opening_bracket_offset
|
|
213
|
+
source = workflow_file.node_source(sequence)
|
|
214
|
+
opening_bracket = T.must(source.b.index("["))
|
|
215
|
+
node_start_offset(sequence) + opening_bracket
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
sig { returns(Integer) }
|
|
219
|
+
def closing_bracket_offset
|
|
220
|
+
source = workflow_file.node_source(sequence)
|
|
221
|
+
closing_bracket = T.must(source.b.rindex("]"))
|
|
222
|
+
node_start_offset(sequence) + closing_bracket
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
sig { params(node: Psych::Nodes::Node).returns(Integer) }
|
|
226
|
+
def node_start_offset(node)
|
|
227
|
+
workflow_file.offset(
|
|
228
|
+
workflow_file.node_start_line(node),
|
|
229
|
+
workflow_file.node_start_column(node)
|
|
230
|
+
)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
sig { params(node: Psych::Nodes::Node).returns(Integer) }
|
|
234
|
+
def node_end_offset(node)
|
|
235
|
+
workflow_file.offset(
|
|
236
|
+
workflow_file.node_end_line(node),
|
|
237
|
+
workflow_file.node_end_column(node)
|
|
238
|
+
)
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
@@ -0,0 +1,147 @@
|
|
|
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 SourceCommentLocator
|
|
11
|
+
extend T::Sig
|
|
12
|
+
|
|
13
|
+
class LocatedComment < T::Struct
|
|
14
|
+
const :comment, String
|
|
15
|
+
const :start_offset, Integer
|
|
16
|
+
const :end_offset, Integer
|
|
17
|
+
const :syntax_adjacent, T::Boolean
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
sig do
|
|
21
|
+
params(
|
|
22
|
+
workflow_file: WorkflowFile,
|
|
23
|
+
comment_finder: YamlCommentFinder,
|
|
24
|
+
metadata_builder: WorkflowFile::MetadataBuilder
|
|
25
|
+
).void
|
|
26
|
+
end
|
|
27
|
+
def initialize(workflow_file:, comment_finder:, metadata_builder:)
|
|
28
|
+
@workflow_file = workflow_file
|
|
29
|
+
@comment_finder = comment_finder
|
|
30
|
+
@metadata_builder = metadata_builder
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
sig { params(declaration: WorkflowFile::UsesDeclaration).returns(T.nilable(LocatedComment)) }
|
|
34
|
+
def for_declaration(declaration)
|
|
35
|
+
anchor = metadata_builder.comment_anchor(declaration)
|
|
36
|
+
return unless anchor
|
|
37
|
+
|
|
38
|
+
locate(
|
|
39
|
+
line: anchor.first,
|
|
40
|
+
column: anchor.last,
|
|
41
|
+
owner_sequence: declaration.steps_sequence,
|
|
42
|
+
block_scalar: block_scalar?(declaration.source_node)
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
sig { params(sequence: Psych::Nodes::Sequence).returns(T.nilable(LocatedComment)) }
|
|
47
|
+
def for_sequence(sequence)
|
|
48
|
+
locate(
|
|
49
|
+
line: workflow_file.node_end_line(sequence),
|
|
50
|
+
column: workflow_file.node_end_column(sequence),
|
|
51
|
+
owner_sequence: sequence,
|
|
52
|
+
block_scalar: false
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
sig { params(source: String).returns(T.nilable(String)) }
|
|
57
|
+
def find_line(source)
|
|
58
|
+
comment_finder.find_line(source)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
sig { returns(WorkflowFile) }
|
|
64
|
+
attr_reader :workflow_file
|
|
65
|
+
|
|
66
|
+
sig { returns(YamlCommentFinder) }
|
|
67
|
+
attr_reader :comment_finder
|
|
68
|
+
|
|
69
|
+
sig { returns(WorkflowFile::MetadataBuilder) }
|
|
70
|
+
attr_reader :metadata_builder
|
|
71
|
+
|
|
72
|
+
sig do
|
|
73
|
+
params(
|
|
74
|
+
line: Integer,
|
|
75
|
+
column: Integer,
|
|
76
|
+
owner_sequence: T.nilable(Psych::Nodes::Sequence),
|
|
77
|
+
block_scalar: T::Boolean
|
|
78
|
+
).returns(T.nilable(LocatedComment))
|
|
79
|
+
end
|
|
80
|
+
def locate(line:, column:, owner_sequence:, block_scalar:)
|
|
81
|
+
suffix = workflow_file.line_body(line).byteslice(workflow_file.byte_column(line, column)..) || ""
|
|
82
|
+
comment = comment_finder.find(suffix)
|
|
83
|
+
return unless comment
|
|
84
|
+
|
|
85
|
+
relative_start = T.must(suffix.b.index(comment.b))
|
|
86
|
+
start_offset = workflow_file.offset(line, column) + relative_start
|
|
87
|
+
return if inside_other_sequence?(start_offset, owner_sequence)
|
|
88
|
+
|
|
89
|
+
prefix = suffix.byteslice(0...relative_start) || ""
|
|
90
|
+
LocatedComment.new(
|
|
91
|
+
comment: comment,
|
|
92
|
+
start_offset: start_offset,
|
|
93
|
+
end_offset: start_offset + comment.bytesize,
|
|
94
|
+
syntax_adjacent: adjacent_prefix?(prefix, block_scalar)
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
sig do
|
|
99
|
+
params(
|
|
100
|
+
offset: Integer,
|
|
101
|
+
owner_sequence: T.nilable(Psych::Nodes::Sequence)
|
|
102
|
+
).returns(T::Boolean)
|
|
103
|
+
end
|
|
104
|
+
def inside_other_sequence?(offset, owner_sequence)
|
|
105
|
+
workflow_file.sequences.any? do |sequence|
|
|
106
|
+
next false if owner_sequence.equal?(sequence)
|
|
107
|
+
|
|
108
|
+
node_start_offset(sequence) <= offset && offset < node_end_offset(sequence)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
sig { params(prefix: String, block_scalar: T::Boolean).returns(T::Boolean) }
|
|
113
|
+
def adjacent_prefix?(prefix, block_scalar)
|
|
114
|
+
return true if prefix.match?(/\A[ \t,\]\}]*\z/)
|
|
115
|
+
return false unless block_scalar
|
|
116
|
+
|
|
117
|
+
prefix.match?(/\A[>|0-9+-]+[ \t]*\z/)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
sig { params(node: T.nilable(Psych::Nodes::Node)).returns(T::Boolean) }
|
|
121
|
+
def block_scalar?(node)
|
|
122
|
+
return false unless node.is_a?(Psych::Nodes::Scalar)
|
|
123
|
+
|
|
124
|
+
style = workflow_file.scalar_node_style(node)
|
|
125
|
+
[Psych::Nodes::Scalar::LITERAL, Psych::Nodes::Scalar::FOLDED].include?(style)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
sig { params(node: Psych::Nodes::Node).returns(Integer) }
|
|
129
|
+
def node_start_offset(node)
|
|
130
|
+
workflow_file.offset(
|
|
131
|
+
workflow_file.node_start_line(node),
|
|
132
|
+
workflow_file.node_start_column(node)
|
|
133
|
+
)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
sig { params(node: Psych::Nodes::Node).returns(Integer) }
|
|
137
|
+
def node_end_offset(node)
|
|
138
|
+
workflow_file.offset(
|
|
139
|
+
workflow_file.node_end_line(node),
|
|
140
|
+
workflow_file.node_end_column(node)
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|