dependabot-github_actions 0.392.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_fetcher.rb +1 -1
- 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/package/package_details_fetcher.rb +38 -20
- 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,96 @@
|
|
|
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 YamlCommentFinder
|
|
11
|
+
extend T::Sig
|
|
12
|
+
|
|
13
|
+
sig { params(suffix: String).returns(T.nilable(String)) }
|
|
14
|
+
def find(suffix)
|
|
15
|
+
previous_non_whitespace = T.let(nil, T.nilable(String))
|
|
16
|
+
index = 0
|
|
17
|
+
|
|
18
|
+
while index < suffix.length
|
|
19
|
+
character = T.must(suffix[index])
|
|
20
|
+
if quoted_scalar_start?(character, previous_non_whitespace)
|
|
21
|
+
previous_non_whitespace = character
|
|
22
|
+
index = quoted_scalar_end(suffix, index)
|
|
23
|
+
next
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
return comment_with_leading_whitespace(suffix, index) if comment_start?(suffix, index)
|
|
27
|
+
|
|
28
|
+
previous_non_whitespace = character unless character.match?(/\s/)
|
|
29
|
+
index += 1
|
|
30
|
+
end
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
sig { params(suffix: String).returns(T.nilable(String)) }
|
|
35
|
+
def find_immediate(suffix)
|
|
36
|
+
comment = find(suffix)
|
|
37
|
+
return unless comment
|
|
38
|
+
|
|
39
|
+
comment_start = T.must(suffix.b.index(comment.b))
|
|
40
|
+
prefix = suffix.byteslice(0...comment_start) || ""
|
|
41
|
+
comment if prefix.match?(/\A[ \t]*,?[ \t]*\z/)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
sig { params(line: String).returns(T.nilable(String)) }
|
|
45
|
+
def find_line(line)
|
|
46
|
+
stripped = line.lstrip
|
|
47
|
+
return stripped if stripped.start_with?("#")
|
|
48
|
+
|
|
49
|
+
find(line)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
sig { params(character: String, previous_non_whitespace: T.nilable(String)).returns(T::Boolean) }
|
|
55
|
+
def quoted_scalar_start?(character, previous_non_whitespace)
|
|
56
|
+
return false unless character == "'" || character == '"'
|
|
57
|
+
|
|
58
|
+
previous_non_whitespace.nil? || ":,[{?-".include?(previous_non_whitespace)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
sig { params(suffix: String, quote_index: Integer).returns(Integer) }
|
|
62
|
+
def quoted_scalar_end(suffix, quote_index)
|
|
63
|
+
quote = T.must(suffix[quote_index])
|
|
64
|
+
index = quote_index + 1
|
|
65
|
+
|
|
66
|
+
while index < suffix.length
|
|
67
|
+
character = T.must(suffix[index])
|
|
68
|
+
if quote == "'" && character == "'" && suffix[index + 1] == "'"
|
|
69
|
+
index += 2
|
|
70
|
+
elsif quote == '"' && character == "\\"
|
|
71
|
+
index += 2
|
|
72
|
+
elsif character == quote
|
|
73
|
+
return index + 1
|
|
74
|
+
else
|
|
75
|
+
index += 1
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
suffix.length
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
sig { params(suffix: String, index: Integer).returns(T::Boolean) }
|
|
82
|
+
def comment_start?(suffix, index)
|
|
83
|
+
suffix[index] == "#" && index.positive? && T.must(suffix[index - 1]).match?(/\s/)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
sig { params(suffix: String, hash_index: Integer).returns(String) }
|
|
87
|
+
def comment_with_leading_whitespace(suffix, hash_index)
|
|
88
|
+
comment_start = hash_index
|
|
89
|
+
comment_start -= 1 while comment_start.positive? && T.must(suffix[comment_start - 1]).match?(/\s/)
|
|
90
|
+
T.must(suffix[comment_start..])
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "dependabot/github_actions/file_updater"
|
|
5
|
+
|
|
6
|
+
module Dependabot
|
|
7
|
+
module GithubActions
|
|
8
|
+
class FileUpdater < Dependabot::FileUpdaters::Base
|
|
9
|
+
class WorkflowUpdater
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
class SourceUpdate < T::Struct
|
|
13
|
+
const :declaration, WorkflowFile::UsesDeclaration
|
|
14
|
+
const :old_declaration, String
|
|
15
|
+
const :new_declaration, String
|
|
16
|
+
const :old_ref, String
|
|
17
|
+
const :new_ref, String
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class ContentEdit < T::Struct
|
|
21
|
+
const :start_offset, Integer
|
|
22
|
+
const :end_offset, Integer
|
|
23
|
+
const :replacement, String
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
require_relative "workflow_updater/version_commenter"
|
|
27
|
+
require_relative "workflow_updater/yaml_comment_finder"
|
|
28
|
+
require_relative "workflow_updater/source_comment_locator"
|
|
29
|
+
require_relative "workflow_updater/source_comment_updater"
|
|
30
|
+
require_relative "workflow_updater/flow_sequence_comments"
|
|
31
|
+
require_relative "workflow_updater/source_updater"
|
|
32
|
+
|
|
33
|
+
sig do
|
|
34
|
+
params(
|
|
35
|
+
file: Dependabot::DependencyFile,
|
|
36
|
+
dependency: Dependabot::Dependency,
|
|
37
|
+
commenter: VersionCommenter
|
|
38
|
+
).void
|
|
39
|
+
end
|
|
40
|
+
def initialize(file:, dependency:, commenter:)
|
|
41
|
+
@file = file
|
|
42
|
+
@dependency = dependency
|
|
43
|
+
@commenter = commenter
|
|
44
|
+
@comment_finder = T.let(YamlCommentFinder.new, YamlCommentFinder)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
sig { returns(String) }
|
|
48
|
+
def updated_content
|
|
49
|
+
workflow_file = WorkflowFile.new(T.must(file.content))
|
|
50
|
+
source_updates, fallback_pairs = requirement_updates(workflow_file)
|
|
51
|
+
|
|
52
|
+
content = SourceUpdater.new(
|
|
53
|
+
workflow_file: workflow_file,
|
|
54
|
+
updates: source_updates,
|
|
55
|
+
commenter: commenter,
|
|
56
|
+
comment_finder: comment_finder
|
|
57
|
+
).updated_content
|
|
58
|
+
fallback_pairs.each do |new_req, old_req|
|
|
59
|
+
content = apply_text_update(content, new_req, old_req)
|
|
60
|
+
end
|
|
61
|
+
content
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
sig { returns(Dependabot::DependencyFile) }
|
|
67
|
+
attr_reader :file
|
|
68
|
+
|
|
69
|
+
sig { returns(Dependabot::Dependency) }
|
|
70
|
+
attr_reader :dependency
|
|
71
|
+
|
|
72
|
+
sig { returns(VersionCommenter) }
|
|
73
|
+
attr_reader :commenter
|
|
74
|
+
|
|
75
|
+
sig { returns(YamlCommentFinder) }
|
|
76
|
+
attr_reader :comment_finder
|
|
77
|
+
|
|
78
|
+
sig do
|
|
79
|
+
params(workflow_file: WorkflowFile).returns(
|
|
80
|
+
[
|
|
81
|
+
T::Array[SourceUpdate],
|
|
82
|
+
T::Array[[Dependabot::DependencyRequirement, Dependabot::DependencyRequirement]]
|
|
83
|
+
]
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
def requirement_updates(workflow_file)
|
|
87
|
+
source_updates = T.let([], T::Array[SourceUpdate])
|
|
88
|
+
fallback_pairs = T.let(
|
|
89
|
+
[],
|
|
90
|
+
T::Array[[Dependabot::DependencyRequirement, Dependabot::DependencyRequirement]]
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
dependency.requirements.zip(T.must(dependency.previous_requirements)).each do |new_req, old_req|
|
|
94
|
+
previous_requirement = T.must(old_req)
|
|
95
|
+
next if new_req.file != file.name || new_req.source == previous_requirement.source
|
|
96
|
+
next unless new_req.source_string("type") == "git"
|
|
97
|
+
|
|
98
|
+
source_update = source_update_for(workflow_file, new_req, previous_requirement)
|
|
99
|
+
source_update ? source_updates << source_update : fallback_pairs << [new_req, previous_requirement]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
[source_updates, fallback_pairs]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
sig do
|
|
106
|
+
params(
|
|
107
|
+
workflow_file: WorkflowFile,
|
|
108
|
+
new_req: Dependabot::DependencyRequirement,
|
|
109
|
+
old_req: Dependabot::DependencyRequirement
|
|
110
|
+
).returns(T.nilable(SourceUpdate))
|
|
111
|
+
end
|
|
112
|
+
def source_update_for(workflow_file, new_req, old_req)
|
|
113
|
+
path = yaml_source_path(old_req)
|
|
114
|
+
return unless path
|
|
115
|
+
|
|
116
|
+
declaration = workflow_file.declaration_at(path)
|
|
117
|
+
return unless declaration
|
|
118
|
+
return unless declaration.value_node && declaration.source_node && declaration.mapping_node
|
|
119
|
+
|
|
120
|
+
old_declaration = T.must(old_req.metadata_string("declaration_string"))
|
|
121
|
+
new_ref = T.must(new_req.source_string("ref"))
|
|
122
|
+
SourceUpdate.new(
|
|
123
|
+
declaration: declaration,
|
|
124
|
+
old_declaration: old_declaration,
|
|
125
|
+
new_declaration: old_declaration.gsub(/@.*+/, "@#{new_ref}"),
|
|
126
|
+
old_ref: T.must(old_req.source_string("ref")),
|
|
127
|
+
new_ref: new_ref
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
sig { params(requirement: Dependabot::DependencyRequirement).returns(T.nilable(WorkflowFile::Path)) }
|
|
132
|
+
def yaml_source_path(requirement)
|
|
133
|
+
metadata = requirement.metadata
|
|
134
|
+
return unless metadata
|
|
135
|
+
|
|
136
|
+
raw_source = metadata[:yaml_source] || metadata["yaml_source"]
|
|
137
|
+
return unless raw_source.is_a?(Hash)
|
|
138
|
+
|
|
139
|
+
raw_path = raw_source[:path] || raw_source["path"]
|
|
140
|
+
return unless raw_path.is_a?(Array)
|
|
141
|
+
return unless raw_path.all? { |part| part.is_a?(String) || part.is_a?(Integer) }
|
|
142
|
+
|
|
143
|
+
path = T.let([], WorkflowFile::Path)
|
|
144
|
+
raw_path.each { |part| path << part }
|
|
145
|
+
path
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
sig do
|
|
149
|
+
params(
|
|
150
|
+
content: String,
|
|
151
|
+
new_req: Dependabot::DependencyRequirement,
|
|
152
|
+
old_req: Dependabot::DependencyRequirement
|
|
153
|
+
).returns(String)
|
|
154
|
+
end
|
|
155
|
+
def apply_text_update(content, new_req, old_req)
|
|
156
|
+
old_ref = T.must(old_req.source_string("ref"))
|
|
157
|
+
new_ref = T.must(new_req.source_string("ref"))
|
|
158
|
+
old_declaration = T.must(old_req.metadata_string("declaration_string"))
|
|
159
|
+
new_declaration = old_declaration.gsub(/@.*+/, "@#{new_ref}")
|
|
160
|
+
declaration_pattern = uses_declaration_pattern(old_declaration)
|
|
161
|
+
|
|
162
|
+
content.gsub(/#{declaration_pattern}(?<suffix>[^\r\n]*)/) do |match|
|
|
163
|
+
updated_workflow_line(
|
|
164
|
+
match: match,
|
|
165
|
+
comment: comment_finder.find(T.must(Regexp.last_match(:suffix))),
|
|
166
|
+
declaration_pattern: declaration_pattern,
|
|
167
|
+
old_declaration: old_declaration,
|
|
168
|
+
new_declaration: new_declaration,
|
|
169
|
+
old_ref: old_ref,
|
|
170
|
+
new_ref: new_ref
|
|
171
|
+
)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
sig { params(declaration: String).returns(Regexp) }
|
|
176
|
+
def uses_declaration_pattern(declaration)
|
|
177
|
+
escaped_declaration = Regexp.escape(declaration)
|
|
178
|
+
|
|
179
|
+
/
|
|
180
|
+
(?:^|[ \t{,])
|
|
181
|
+
(?:"uses"|'uses'|uses)[ \t]*:[ \t]*
|
|
182
|
+
(?:"#{escaped_declaration}"|'#{escaped_declaration}'|#{escaped_declaration})
|
|
183
|
+
(?=[ \t]|$|[,}\]])
|
|
184
|
+
/x
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
sig do
|
|
188
|
+
params(
|
|
189
|
+
match: String,
|
|
190
|
+
comment: T.nilable(String),
|
|
191
|
+
declaration_pattern: Regexp,
|
|
192
|
+
old_declaration: String,
|
|
193
|
+
new_declaration: String,
|
|
194
|
+
old_ref: String,
|
|
195
|
+
new_ref: String
|
|
196
|
+
).returns(String)
|
|
197
|
+
end
|
|
198
|
+
def updated_workflow_line(
|
|
199
|
+
match:,
|
|
200
|
+
comment:,
|
|
201
|
+
declaration_pattern:,
|
|
202
|
+
old_declaration:,
|
|
203
|
+
new_declaration:,
|
|
204
|
+
old_ref:,
|
|
205
|
+
new_ref:
|
|
206
|
+
)
|
|
207
|
+
line_without_comment = comment ? match.delete_suffix(comment) : match
|
|
208
|
+
updated_match = line_without_comment.gsub(declaration_pattern) do |declaration|
|
|
209
|
+
declaration.sub(old_declaration, new_declaration)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
if comment
|
|
213
|
+
updated_match << (commenter.updated_comment(comment, old_ref, new_ref) || comment)
|
|
214
|
+
elsif (new_comment = commenter.new_comment(old_ref, new_ref))
|
|
215
|
+
updated_match << new_comment
|
|
216
|
+
end
|
|
217
|
+
updated_match
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
@@ -8,12 +8,15 @@ require "dependabot/file_updaters"
|
|
|
8
8
|
require "dependabot/file_updaters/base"
|
|
9
9
|
require "dependabot/github_actions/constants"
|
|
10
10
|
require "dependabot/github_actions/lockfile"
|
|
11
|
+
require "dependabot/github_actions/workflow_file"
|
|
11
12
|
|
|
12
13
|
module Dependabot
|
|
13
14
|
module GithubActions
|
|
14
15
|
class FileUpdater < Dependabot::FileUpdaters::Base
|
|
15
16
|
extend T::Sig
|
|
16
17
|
|
|
18
|
+
require_relative "file_updater/workflow_updater"
|
|
19
|
+
|
|
17
20
|
sig { override.returns(T::Array[Dependabot::DependencyFile]) }
|
|
18
21
|
def updated_dependency_files
|
|
19
22
|
updated_files = changed_workflow_files.map do |file|
|
|
@@ -138,85 +141,24 @@ module Dependabot
|
|
|
138
141
|
file.path.delete_prefix("/")
|
|
139
142
|
end
|
|
140
143
|
|
|
141
|
-
# rubocop:disable Metrics/AbcSize
|
|
142
144
|
sig { params(file: Dependabot::DependencyFile).returns(String) }
|
|
143
145
|
def updated_workflow_file_content(file)
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
old_declaration = T.must(T.must(old_req).metadata_string("declaration_string"))
|
|
162
|
-
new_declaration =
|
|
163
|
-
old_declaration
|
|
164
|
-
.gsub(/@.*+/, "@#{new_ref}")
|
|
165
|
-
|
|
166
|
-
# Replace the old declaration that's preceded by a non-word character (unless it's a hyphen)
|
|
167
|
-
# and followed by a whitespace character (comments) or EOL.
|
|
168
|
-
# If the declaration is followed by a comment that lists the version associated
|
|
169
|
-
# with the SHA source ref, then update the comment to the human-readable new version.
|
|
170
|
-
# However, if the comment includes additional text beyond the version, for safety
|
|
171
|
-
# we skip updating the comment in case it's a custom note, todo, warning etc of some kind.
|
|
172
|
-
# See the related unit tests for examples.
|
|
173
|
-
updated_content =
|
|
174
|
-
updated_content
|
|
175
|
-
.gsub(
|
|
176
|
-
/(?<=[^a-zA-Z_-]|"|')#{Regexp.escape(old_declaration)}["']?(?<comment>\s+#.*)?(?=\s|$)/
|
|
177
|
-
) do |match|
|
|
178
|
-
comment = Regexp.last_match(:comment)
|
|
179
|
-
match.gsub!(old_declaration, new_declaration)
|
|
180
|
-
if comment && (updated_comment = updated_version_comment(comment, old_ref, new_ref))
|
|
181
|
-
match.gsub!(comment, updated_comment)
|
|
182
|
-
end
|
|
183
|
-
match
|
|
184
|
-
end
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
updated_content
|
|
188
|
-
end
|
|
189
|
-
# rubocop:enable Metrics/AbcSize
|
|
190
|
-
|
|
191
|
-
sig { params(comment: T.nilable(String), old_ref: String, new_ref: String).returns(T.nilable(String)) }
|
|
192
|
-
def updated_version_comment(comment, old_ref, new_ref)
|
|
193
|
-
raise "No comment!" unless comment
|
|
194
|
-
|
|
195
|
-
comment = comment.rstrip
|
|
196
|
-
git_checker = Dependabot::GitCommitChecker.new(dependency: dependency, credentials: credentials)
|
|
197
|
-
return unless git_checker.ref_looks_like_commit_sha?(old_ref)
|
|
198
|
-
|
|
199
|
-
previous_version_tags = git_checker.most_specific_version_tags_for_sha(old_ref)
|
|
200
|
-
return unless previous_version_tags.any? # There's no tag for this commit
|
|
201
|
-
|
|
202
|
-
# Use the most specific (longest) matching version to avoid partial replacements.
|
|
203
|
-
# Tags are sorted ascending, so ["v1", "v1.0", "v1.0.1"] maps to ["1", "1.0", "1.0.1"].
|
|
204
|
-
# Without this, "1" could match the end of "v1.0.1", causing gsub("1", "1.1") => "v1.1.0.1.1".
|
|
205
|
-
previous_version = previous_version_tags.map { |tag| version_class.new(tag).to_s }
|
|
206
|
-
.select { |version| comment.end_with?(version) }
|
|
207
|
-
.max_by(&:length)
|
|
208
|
-
return unless previous_version
|
|
209
|
-
|
|
210
|
-
new_version_tag = git_checker.most_specific_version_tag_for_sha(new_ref)
|
|
211
|
-
return unless new_version_tag
|
|
212
|
-
|
|
213
|
-
new_version = version_class.new(new_version_tag).to_s
|
|
214
|
-
comment.gsub(previous_version, new_version)
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
sig { returns(T.class_of(Dependabot::GithubActions::Version)) }
|
|
218
|
-
def version_class
|
|
219
|
-
GithubActions::Version
|
|
146
|
+
WorkflowUpdater.new(
|
|
147
|
+
file: file,
|
|
148
|
+
dependency: dependency,
|
|
149
|
+
commenter: version_commenter
|
|
150
|
+
).updated_content
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
sig { returns(WorkflowUpdater::VersionCommenter) }
|
|
154
|
+
def version_commenter
|
|
155
|
+
@version_commenter ||= T.let(
|
|
156
|
+
WorkflowUpdater::VersionCommenter.new(
|
|
157
|
+
dependency: dependency,
|
|
158
|
+
credentials: credentials
|
|
159
|
+
),
|
|
160
|
+
T.nilable(WorkflowUpdater::VersionCommenter)
|
|
161
|
+
)
|
|
220
162
|
end
|
|
221
163
|
end
|
|
222
164
|
end
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# typed:
|
|
1
|
+
# typed: strong
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require "json"
|
|
@@ -78,14 +78,16 @@ module Dependabot
|
|
|
78
78
|
# If the dependency is pinned to a tag that looks like a version then
|
|
79
79
|
# we want to update that tag.
|
|
80
80
|
if git_commit_checker.pinned_ref_looks_like_version? && latest_version_tag
|
|
81
|
-
latest_version = latest_version_tag
|
|
81
|
+
latest_version = tag_version(T.must(latest_version_tag))
|
|
82
|
+
return unless latest_version.is_a?(Dependabot::Version)
|
|
82
83
|
return current_version if shortened_semver_eq?(dependency.version, latest_version.to_s)
|
|
83
84
|
|
|
84
85
|
return latest_version
|
|
85
86
|
end
|
|
86
87
|
|
|
87
88
|
if git_commit_checker.pinned_ref_looks_like_commit_sha? && latest_version_tag
|
|
88
|
-
latest_version = latest_version_tag
|
|
89
|
+
latest_version = tag_version(T.must(latest_version_tag))
|
|
90
|
+
return unless latest_version.is_a?(Dependabot::Version)
|
|
89
91
|
return latest_commit_for_pinned_ref unless git_commit_checker.local_tag_for_pinned_sha
|
|
90
92
|
|
|
91
93
|
return latest_version
|
|
@@ -97,7 +99,7 @@ module Dependabot
|
|
|
97
99
|
end
|
|
98
100
|
# rubocop:enable Metrics/PerceivedComplexity
|
|
99
101
|
|
|
100
|
-
sig { returns(T.nilable(T::Hash[Symbol,
|
|
102
|
+
sig { returns(T.nilable(T::Hash[Symbol, Object])) }
|
|
101
103
|
def lowest_security_fix_version_tag
|
|
102
104
|
# TODO: Support Docker sources
|
|
103
105
|
return unless git_dependency?
|
|
@@ -113,25 +115,27 @@ module Dependabot
|
|
|
113
115
|
find_lowest_secure_version(tags)
|
|
114
116
|
end
|
|
115
117
|
end,
|
|
116
|
-
T.nilable(T::Hash[Symbol,
|
|
118
|
+
T.nilable(T::Hash[Symbol, Object])
|
|
117
119
|
)
|
|
118
120
|
end
|
|
119
121
|
|
|
120
|
-
sig { returns(T.nilable(T::Hash[Symbol,
|
|
122
|
+
sig { returns(T.nilable(T::Hash[Symbol, Object])) }
|
|
121
123
|
def latest_version_tag
|
|
122
124
|
@latest_version_tag ||= T.let(
|
|
123
125
|
begin
|
|
124
126
|
return git_commit_checker.local_tag_for_latest_version if dependency.version.nil?
|
|
125
127
|
|
|
126
128
|
ref = git_commit_checker.local_ref_for_latest_version_matching_existing_precision
|
|
127
|
-
return ref if ref && ref.
|
|
129
|
+
return ref if ref&.version && T.must(ref.version) > current_version
|
|
128
130
|
|
|
129
131
|
lower_precision_ref = git_commit_checker.local_ref_for_latest_version_lower_precision
|
|
130
|
-
|
|
132
|
+
# Keep the matching-precision ref when no newer lower-precision tag exists
|
|
133
|
+
return ref if ref&.version == current_version &&
|
|
134
|
+
(lower_precision_ref.nil? || T.must(lower_precision_ref.version) <= current_version)
|
|
131
135
|
|
|
132
136
|
lower_precision_ref
|
|
133
137
|
end,
|
|
134
|
-
T.nilable(T::Hash[Symbol,
|
|
138
|
+
T.nilable(T::Hash[Symbol, Object])
|
|
135
139
|
)
|
|
136
140
|
end
|
|
137
141
|
|
|
@@ -159,9 +163,7 @@ module Dependabot
|
|
|
159
163
|
[]
|
|
160
164
|
end
|
|
161
165
|
|
|
162
|
-
sig
|
|
163
|
-
returns(T::Array[T::Hash[Symbol, T.untyped]])
|
|
164
|
-
end
|
|
166
|
+
sig { returns(T::Array[T::Hash[Symbol, Object]]) }
|
|
165
167
|
def allowed_version_tags_with_release_dates
|
|
166
168
|
allowed_version_tags_hashes = git_commit_checker.local_tags_for_allowed_versions
|
|
167
169
|
tag_to_release_date = T.let({}, T::Hash[String, T.nilable(String)])
|
|
@@ -173,14 +175,14 @@ module Dependabot
|
|
|
173
175
|
|
|
174
176
|
# Combine version info with release dates and sort by version descending
|
|
175
177
|
result = allowed_version_tags_hashes.map do |tag_hash|
|
|
176
|
-
tag_name = tag_hash
|
|
178
|
+
tag_name = tag_name(tag_hash)
|
|
177
179
|
tag_hash.merge(
|
|
178
180
|
release_date: tag_to_release_date[tag_name]
|
|
179
181
|
)
|
|
180
182
|
end
|
|
181
183
|
|
|
182
184
|
# Sort by version descending (newest first)
|
|
183
|
-
result.sort_by { |tag_hash| tag_hash.fetch(:version) }.reverse
|
|
185
|
+
result.sort_by { |tag_hash| T.cast(tag_hash.fetch(:version), Gem::Version) }.reverse
|
|
184
186
|
end
|
|
185
187
|
|
|
186
188
|
private
|
|
@@ -244,23 +246,39 @@ module Dependabot
|
|
|
244
246
|
end
|
|
245
247
|
|
|
246
248
|
sig do
|
|
247
|
-
params(tags: T::Array[T::Hash[Symbol,
|
|
249
|
+
params(tags: T::Array[T::Hash[Symbol, Object]]).returns(T.nilable(T::Hash[Symbol, Object]))
|
|
248
250
|
end
|
|
249
251
|
def find_lowest_secure_version(tags)
|
|
250
|
-
relevant_tags =
|
|
251
|
-
|
|
252
|
+
relevant_tags = tags.reject do |tag|
|
|
253
|
+
security_advisories.any? { |advisory| advisory.vulnerable?(T.must(tag_version(tag))) }
|
|
254
|
+
end
|
|
252
255
|
|
|
253
256
|
relevant_tags = filter_lower_tags(relevant_tags)
|
|
254
|
-
relevant_tags.min_by { |tag|
|
|
257
|
+
relevant_tags.min_by { |tag| T.must(tag_version(tag)) }
|
|
255
258
|
end
|
|
256
259
|
|
|
257
260
|
sig do
|
|
258
|
-
params(tags_array: T::Array[T::Hash[Symbol,
|
|
261
|
+
params(tags_array: T::Array[T::Hash[Symbol, Object]]).returns(T::Array[T::Hash[Symbol, Object]])
|
|
259
262
|
end
|
|
260
263
|
def filter_lower_tags(tags_array)
|
|
261
264
|
return tags_array unless current_version
|
|
262
265
|
|
|
263
|
-
tags_array.select { |tag|
|
|
266
|
+
tags_array.select { |tag| T.must(tag_version(tag)) > current_version }
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
sig { params(tag: T::Hash[Symbol, Object]).returns(String) }
|
|
270
|
+
def tag_name(tag)
|
|
271
|
+
return tag.tag if tag.is_a?(Dependabot::GitTagDetails)
|
|
272
|
+
|
|
273
|
+
T.cast(tag.fetch(:tag), String)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
sig { params(tag: T::Hash[Symbol, Object]).returns(T.nilable(Gem::Version)) }
|
|
277
|
+
def tag_version(tag)
|
|
278
|
+
return tag.version if tag.is_a?(Dependabot::GitTagDetails)
|
|
279
|
+
|
|
280
|
+
version = tag[:version]
|
|
281
|
+
version if version.is_a?(Gem::Version)
|
|
264
282
|
end
|
|
265
283
|
|
|
266
284
|
sig { returns(T::Boolean) }
|
|
@@ -59,7 +59,7 @@ module Dependabot
|
|
|
59
59
|
|
|
60
60
|
sig { override.returns(T::Boolean) }
|
|
61
61
|
def up_to_date?
|
|
62
|
-
super && !
|
|
62
|
+
super && !relevant_requirements_changed?
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
sig { override.returns(T::Array[Dependabot::DependencyRequirement]) }
|
|
@@ -82,16 +82,36 @@ module Dependabot
|
|
|
82
82
|
return true if super
|
|
83
83
|
return false unless requirements_to_unlock == :own
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
relevant_requirements_changed?
|
|
86
86
|
end
|
|
87
87
|
|
|
88
88
|
sig { returns(T::Boolean) }
|
|
89
|
-
def
|
|
89
|
+
def relevant_requirements_changed?
|
|
90
90
|
dependency.requirements.zip(updated_requirements).any? do |current, updated|
|
|
91
|
-
|
|
91
|
+
next false unless updated
|
|
92
|
+
|
|
93
|
+
current != updated && (onboarded_requirement?(current) || tag_to_sha_requirement?(current, updated))
|
|
92
94
|
end
|
|
93
95
|
end
|
|
94
96
|
|
|
97
|
+
sig do
|
|
98
|
+
params(
|
|
99
|
+
current: Dependabot::DependencyRequirement,
|
|
100
|
+
updated: Dependabot::DependencyRequirement
|
|
101
|
+
).returns(T::Boolean)
|
|
102
|
+
end
|
|
103
|
+
def tag_to_sha_requirement?(current, updated)
|
|
104
|
+
return false unless pin_to_sha?
|
|
105
|
+
|
|
106
|
+
current_ref = current.source_string("ref")
|
|
107
|
+
updated_ref = updated.source_string("ref")
|
|
108
|
+
return false unless current_ref && updated_ref
|
|
109
|
+
return false unless version_class.correct?(current_ref)
|
|
110
|
+
return false if Dependabot::GithubActions::Version.path_based?(current_ref)
|
|
111
|
+
|
|
112
|
+
git_commit_checker.ref_looks_like_commit_sha?(updated_ref)
|
|
113
|
+
end
|
|
114
|
+
|
|
95
115
|
# A requirement is "onboarded" when the repo carries an `actions.lock` that is
|
|
96
116
|
# authoritative for the requirement's workflow. Only onboarded requirements get
|
|
97
117
|
# per-source precision selection; everything else flows through the combined
|
|
@@ -246,11 +266,12 @@ module Dependabot
|
|
|
246
266
|
# TODO: Support Docker sources
|
|
247
267
|
return unless git_commit_checker.git_dependency?
|
|
248
268
|
|
|
249
|
-
git_source =
|
|
269
|
+
git_source = git_source_for(source)
|
|
270
|
+
current_ref = git_source&.fetch(:ref, nil)
|
|
250
271
|
finder = onboarded ? latest_version_finder_for(git_source) : T.must(latest_version_finder)
|
|
251
272
|
|
|
252
273
|
if vulnerable? && (new_tag = finder.lowest_security_fix_release)
|
|
253
|
-
return new_tag
|
|
274
|
+
return ref_for_release(new_tag, current_ref: current_ref)
|
|
254
275
|
end
|
|
255
276
|
|
|
256
277
|
source_git_commit_checker = git_helper.git_commit_checker_for(git_source)
|
|
@@ -258,7 +279,7 @@ module Dependabot
|
|
|
258
279
|
# Return the git tag if updating a pinned version
|
|
259
280
|
if source_git_commit_checker.pinned_ref_looks_like_version? &&
|
|
260
281
|
(new_tag = finder.latest_version_tag_respecting_cooldown)
|
|
261
|
-
return new_tag
|
|
282
|
+
return ref_for_release(new_tag, current_ref: current_ref)
|
|
262
283
|
end
|
|
263
284
|
|
|
264
285
|
# Return the pinned git commit if one is available
|
|
@@ -271,6 +292,34 @@ module Dependabot
|
|
|
271
292
|
nil
|
|
272
293
|
end
|
|
273
294
|
|
|
295
|
+
sig { returns(T::Boolean) }
|
|
296
|
+
def pin_to_sha?
|
|
297
|
+
T.cast(options.fetch(:github_actions_pin_to_sha, false), T::Boolean)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
sig do
|
|
301
|
+
params(
|
|
302
|
+
release: T::Hash[Symbol, Object],
|
|
303
|
+
current_ref: T.nilable(String)
|
|
304
|
+
).returns(String)
|
|
305
|
+
end
|
|
306
|
+
def ref_for_release(release, current_ref:)
|
|
307
|
+
return T.cast(release.fetch(:tag), String) unless pin_to_sha?
|
|
308
|
+
if current_ref && Dependabot::GithubActions::Version.path_based?(current_ref)
|
|
309
|
+
return T.cast(release.fetch(:tag), String)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
T.cast(release.fetch(:commit_sha), String)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
sig do
|
|
316
|
+
params(source: T.nilable(Dependabot::DependencyRequirement::ObjectHash))
|
|
317
|
+
.returns(T.nilable(GitSource))
|
|
318
|
+
end
|
|
319
|
+
def git_source_for(source)
|
|
320
|
+
source && symbolize_source(source)
|
|
321
|
+
end
|
|
322
|
+
|
|
274
323
|
sig do
|
|
275
324
|
params(source: Dependabot::DependencyRequirement::ObjectHash)
|
|
276
325
|
.returns(GitSource)
|