dependabot-julia 0.386.0 → 0.387.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/julia/file_fetcher.rb +4 -1
- data/lib/dependabot/julia/file_parser.rb +98 -1
- data/lib/dependabot/julia/file_updater.rb +121 -73
- data/lib/dependabot/julia/metadata_finder.rb +6 -28
- data/lib/dependabot/julia/package/package_details_fetcher.rb +17 -21
- data/lib/dependabot/julia/package_manager.rb +1 -1
- data/lib/dependabot/julia/registry_client.rb +79 -50
- data/lib/dependabot/julia/requirement.rb +28 -14
- data/lib/dependabot/julia/update_checker/latest_version_finder.rb +48 -16
- data/lib/dependabot/julia/update_checker/requirements_updater.rb +20 -5
- data/lib/dependabot/julia/update_checker.rb +17 -11
- data/lib/dependabot/julia/version.rb +33 -5
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 76dfbce20089045f1450b8e8c2c48a7b3c9b57fb48036014d0df8408a22c2603
|
|
4
|
+
data.tar.gz: f3594883c239208218847751d87eecbd3f04f9af019e4f9044d8a7da57ee6ac5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 27f432eee3b36aa258ae409c4afe26aaa4bc8f41589025d1f4147ba4737d4fe0543225c8765178797e5dbc6c66728e93dec966fd8849223b3731b42e29034dce
|
|
7
|
+
data.tar.gz: 6b414d534a79f3d9098878652fd5554091cd3faccdb36cab1236ce6972f74e3a24cc7ac68986b46276829b178961132d16971eb2285b47cecc658a23a572246b
|
|
@@ -45,8 +45,11 @@ module Dependabot
|
|
|
45
45
|
fetched_files
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
-
sig { params(workspace_info: T::Hash[String,
|
|
48
|
+
sig { params(workspace_info: T::Hash[String, Object]).void }
|
|
49
49
|
def validate_workspace_info!(workspace_info)
|
|
50
|
+
# Only domain errors ("no project file found") reach here; helper
|
|
51
|
+
# crashes raise from RegistryClient rather than being misreported as
|
|
52
|
+
# a missing Project.toml.
|
|
50
53
|
error_value = T.cast(workspace_info["error"], T.nilable(String))
|
|
51
54
|
has_error = !error_value.nil?
|
|
52
55
|
project_files = T.cast(workspace_info["project_files"], T.nilable(T::Array[String]))
|
|
@@ -110,13 +110,87 @@ module Dependabot
|
|
|
110
110
|
parse_single_project_file(proj_file, dependencies_map)
|
|
111
111
|
end
|
|
112
112
|
|
|
113
|
+
apply_manifest_versions(dependencies_map)
|
|
114
|
+
|
|
113
115
|
dependencies_map.values
|
|
114
116
|
end
|
|
115
117
|
|
|
118
|
+
# Resolve the installed version of each dependency from the manifest
|
|
119
|
+
# (Julia's lockfile), matching by UUID.
|
|
120
|
+
sig { params(dependencies_map: T::Hash[String, Dependabot::Dependency]).void }
|
|
121
|
+
def apply_manifest_versions(dependencies_map)
|
|
122
|
+
versions = manifest_versions_by_uuid
|
|
123
|
+
return if versions.empty?
|
|
124
|
+
|
|
125
|
+
dependencies_map.transform_values! do |dep|
|
|
126
|
+
uuid = T.cast(dep.metadata[:julia_uuid], T.nilable(String))
|
|
127
|
+
version = uuid && versions[uuid]
|
|
128
|
+
next dep unless version
|
|
129
|
+
|
|
130
|
+
Dependabot::Dependency.new(
|
|
131
|
+
name: dep.name,
|
|
132
|
+
version: version,
|
|
133
|
+
requirements: dep.requirements,
|
|
134
|
+
package_manager: "julia",
|
|
135
|
+
metadata: dep.metadata
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
sig { returns(T::Hash[String, String]) }
|
|
141
|
+
def manifest_versions_by_uuid
|
|
142
|
+
manifest = manifest_file
|
|
143
|
+
return {} unless manifest
|
|
144
|
+
|
|
145
|
+
result = parse_manifest_content(T.must(manifest.content))
|
|
146
|
+
|
|
147
|
+
if result["error"]
|
|
148
|
+
Dependabot.logger.warn("Failed to parse Julia manifest: #{result['error']}")
|
|
149
|
+
return {}
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
deps = T.cast(result["dependencies"] || [], T::Array[T.untyped])
|
|
153
|
+
deps.each_with_object({}) do |dep_info, map|
|
|
154
|
+
dep_hash = T.cast(dep_info, T::Hash[String, T.untyped])
|
|
155
|
+
uuid = T.cast(dep_hash["uuid"], T.nilable(String))
|
|
156
|
+
version = T.cast(dep_hash["version"], T.nilable(String)).to_s
|
|
157
|
+
next if uuid.nil? || version.empty?
|
|
158
|
+
|
|
159
|
+
map[uuid] = version
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
sig { params(content: String).returns(T::Hash[String, T.untyped]) }
|
|
164
|
+
def parse_manifest_content(content)
|
|
165
|
+
result = T.let(nil, T.nilable(T::Hash[String, T.untyped]))
|
|
166
|
+
Dir.mktmpdir("julia_manifest") do |temp_dir|
|
|
167
|
+
# Written under a fixed name: version-suffixed manifests
|
|
168
|
+
# (Manifest-v1.11.toml) would otherwise be skipped by Pkg when the
|
|
169
|
+
# helper's Julia version doesn't match.
|
|
170
|
+
manifest_path = File.join(temp_dir, "Manifest.toml")
|
|
171
|
+
File.write(manifest_path, content)
|
|
172
|
+
result = registry_client.parse_manifest(manifest_path)
|
|
173
|
+
end
|
|
174
|
+
T.must(result)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
|
178
|
+
def manifest_file
|
|
179
|
+
dependency_files.find do |f|
|
|
180
|
+
File.basename(f.name).match?(/^(Julia)?Manifest(?:-v[\d.]+)?\.toml$/i)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
116
184
|
sig { params(proj_file: Dependabot::DependencyFile, dependencies_map: T::Hash[String, Dependabot::Dependency]).void }
|
|
117
185
|
def parse_single_project_file(proj_file, dependencies_map)
|
|
118
186
|
temp_dir = Dir.mktmpdir("julia_project")
|
|
119
|
-
|
|
187
|
+
# File names like "../Project.toml" (a workspace root fetched from a
|
|
188
|
+
# member directory) must not escape the temp dir; fall back to the
|
|
189
|
+
# basename since each project file gets its own directory anyway.
|
|
190
|
+
project_path = File.expand_path(File.join(temp_dir, proj_file.name))
|
|
191
|
+
unless project_path.start_with?("#{File.expand_path(temp_dir)}#{File::SEPARATOR}")
|
|
192
|
+
project_path = File.join(temp_dir, File.basename(proj_file.name))
|
|
193
|
+
end
|
|
120
194
|
FileUtils.mkdir_p(File.dirname(project_path))
|
|
121
195
|
File.write(project_path, proj_file.content)
|
|
122
196
|
|
|
@@ -164,6 +238,8 @@ module Dependabot
|
|
|
164
238
|
if dependencies_map.key?(name)
|
|
165
239
|
# Merge requirements from additional project files
|
|
166
240
|
existing_dep = T.must(dependencies_map[name])
|
|
241
|
+
next if uuid_conflict?(existing_dep, uuid, file_name)
|
|
242
|
+
|
|
167
243
|
existing_requirements = existing_dep.requirements + [new_requirement]
|
|
168
244
|
dependencies_map[name] = Dependabot::Dependency.new(
|
|
169
245
|
name: name,
|
|
@@ -185,6 +261,27 @@ module Dependabot
|
|
|
185
261
|
end
|
|
186
262
|
end
|
|
187
263
|
|
|
264
|
+
# UUID is a package's identity in Julia: two same-named entries with
|
|
265
|
+
# different UUIDs are different packages, and merging them would run
|
|
266
|
+
# updates against the wrong UUID.
|
|
267
|
+
sig do
|
|
268
|
+
params(
|
|
269
|
+
existing_dep: Dependabot::Dependency,
|
|
270
|
+
uuid: T.nilable(String),
|
|
271
|
+
file_name: String
|
|
272
|
+
).returns(T::Boolean)
|
|
273
|
+
end
|
|
274
|
+
def uuid_conflict?(existing_dep, uuid, file_name)
|
|
275
|
+
existing_uuid = T.cast(existing_dep.metadata[:julia_uuid], T.nilable(String))
|
|
276
|
+
return false unless uuid && existing_uuid && uuid != existing_uuid
|
|
277
|
+
|
|
278
|
+
Dependabot.logger.warn(
|
|
279
|
+
"Skipping #{existing_dep.name} in #{file_name}: UUID #{uuid} conflicts with #{existing_uuid} " \
|
|
280
|
+
"from another project file"
|
|
281
|
+
)
|
|
282
|
+
true
|
|
283
|
+
end
|
|
284
|
+
|
|
188
285
|
sig { returns(T::Array[Dependabot::DependencyFile]) }
|
|
189
286
|
def all_project_files
|
|
190
287
|
dependency_files.select { |f| f.name.match?(/Project\.toml$/i) }
|
|
@@ -15,6 +15,10 @@ module Dependabot
|
|
|
15
15
|
class FileUpdater < Dependabot::FileUpdaters::Base
|
|
16
16
|
extend T::Sig
|
|
17
17
|
|
|
18
|
+
# Matches a [compat] table header, tolerating indentation and a trailing
|
|
19
|
+
# comment ("[compat] # pins")
|
|
20
|
+
COMPAT_HEADER_PATTERN = T.let(/^\s*\[compat\]\s*(?:#.*)?$/, Regexp)
|
|
21
|
+
|
|
18
22
|
sig { returns(T::Array[Regexp]) }
|
|
19
23
|
def self.updated_files_regex
|
|
20
24
|
[/(?:Julia)?Project\.toml$/i, /(?:Julia)?Manifest(?:-v[\d.]+)?\.toml$/i]
|
|
@@ -58,6 +62,10 @@ module Dependabot
|
|
|
58
62
|
|
|
59
63
|
return all_projects_only_update(updated_project_files) if actual_manifest.nil?
|
|
60
64
|
|
|
65
|
+
# Requirement-only updates (no target versions) have nothing to tell
|
|
66
|
+
# Pkg; ship the Project.toml changes on their own.
|
|
67
|
+
return all_projects_only_update(updated_project_files) if build_updates_hash.empty?
|
|
68
|
+
|
|
61
69
|
# Write all updated project files to disk for Julia's Pkg
|
|
62
70
|
write_all_temporary_files(updated_project_files, actual_manifest)
|
|
63
71
|
result = call_julia_helper
|
|
@@ -232,19 +240,30 @@ module Dependabot
|
|
|
232
240
|
|
|
233
241
|
private
|
|
234
242
|
|
|
235
|
-
sig { returns(T::Hash[String, String]) }
|
|
243
|
+
sig { returns(T::Hash[String, T::Hash[String, String]]) }
|
|
236
244
|
def build_updates_hash
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
245
|
+
@build_updates_hash ||= T.let(
|
|
246
|
+
begin
|
|
247
|
+
updates = T.let({}, T::Hash[String, T::Hash[String, String]])
|
|
248
|
+
dependencies.each do |dependency|
|
|
249
|
+
version = dependency.version
|
|
250
|
+
next unless version
|
|
251
|
+
|
|
252
|
+
uuid = dependency.metadata[:julia_uuid]
|
|
253
|
+
unless uuid.is_a?(String)
|
|
254
|
+
Dependabot.logger.warn("Skipping manifest update for #{dependency.name}: no UUID available")
|
|
255
|
+
next
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
updates[uuid] = {
|
|
259
|
+
"name" => dependency.name,
|
|
260
|
+
"version" => version
|
|
261
|
+
}
|
|
262
|
+
end
|
|
263
|
+
updates
|
|
264
|
+
end,
|
|
265
|
+
T.nilable(T::Hash[String, T::Hash[String, String]])
|
|
266
|
+
)
|
|
248
267
|
end
|
|
249
268
|
|
|
250
269
|
# Helper methods for DependabotHelper.jl integration
|
|
@@ -253,12 +272,25 @@ module Dependabot
|
|
|
253
272
|
def registry_client
|
|
254
273
|
@registry_client ||= T.let(
|
|
255
274
|
Dependabot::Julia::RegistryClient.new(
|
|
256
|
-
credentials: credentials
|
|
275
|
+
credentials: credentials,
|
|
276
|
+
custom_registries: custom_registries
|
|
257
277
|
),
|
|
258
278
|
T.nilable(Dependabot::Julia::RegistryClient)
|
|
259
279
|
)
|
|
260
280
|
end
|
|
261
281
|
|
|
282
|
+
sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
|
|
283
|
+
def custom_registries
|
|
284
|
+
@custom_registries ||= T.let(
|
|
285
|
+
begin
|
|
286
|
+
registries_config = T.cast(options[:registries], T.nilable(T::Hash[Symbol, T.anything]))
|
|
287
|
+
registries = T.cast(registries_config&.dig(:julia), T.nilable(T::Array[T::Hash[Symbol, T.anything]])) || []
|
|
288
|
+
registries.map { |registry| registry.transform_keys(&:to_sym) }
|
|
289
|
+
end,
|
|
290
|
+
T.nilable(T::Array[T::Hash[Symbol, T.untyped]])
|
|
291
|
+
)
|
|
292
|
+
end
|
|
293
|
+
|
|
262
294
|
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
|
263
295
|
def find_manifest_file
|
|
264
296
|
# The file fetcher has already identified the correct manifest file
|
|
@@ -270,10 +302,17 @@ module Dependabot
|
|
|
270
302
|
dependency_files.find do |f|
|
|
271
303
|
# Use basename to get just the filename, not the full path with ../
|
|
272
304
|
is_manifest = File.basename(f.name).match?(/^(Julia)?Manifest(?:-v[\d.]+)?\.toml$/i)
|
|
273
|
-
is_manifest && (f.directory == project_dir ||
|
|
305
|
+
is_manifest && (f.directory == project_dir || parent_directory_of?(f.directory, project_dir))
|
|
274
306
|
end
|
|
275
307
|
end
|
|
276
308
|
|
|
309
|
+
sig { params(candidate: String, dir: String).returns(T::Boolean) }
|
|
310
|
+
def parent_directory_of?(candidate, dir)
|
|
311
|
+
# Segment-aware prefix check so "/doc" is not treated as a parent of "/docs"
|
|
312
|
+
prefix = candidate.end_with?("/") ? candidate : "#{candidate}/"
|
|
313
|
+
dir.start_with?(prefix)
|
|
314
|
+
end
|
|
315
|
+
|
|
277
316
|
sig { params(manifest_path: String).returns(Dependabot::DependencyFile) }
|
|
278
317
|
def manifest_file_for_path(manifest_path)
|
|
279
318
|
# manifest_path is relative to the project directory (e.g., "Manifest.toml" or "../Manifest.toml")
|
|
@@ -288,10 +327,11 @@ module Dependabot
|
|
|
288
327
|
|
|
289
328
|
# Find the matching manifest file in dependency_files
|
|
290
329
|
found_manifest = dependency_files.find do |f|
|
|
291
|
-
next unless f.name.match?(/^(Julia)?Manifest(?:-v[\d.]+)?\.toml$/i)
|
|
330
|
+
next unless File.basename(f.name).match?(/^(Julia)?Manifest(?:-v[\d.]+)?\.toml$/i)
|
|
292
331
|
|
|
293
|
-
# Construct the full path for this file
|
|
294
|
-
|
|
332
|
+
# Construct the full path for this file, resolving ".." segments
|
|
333
|
+
# (workspace manifests are named e.g. "../Manifest.toml")
|
|
334
|
+
file_path = Pathname.new(File.join(f.directory, f.name)).cleanpath.to_s.sub(%r{^/}, "")
|
|
295
335
|
file_path == resolved_manifest_path
|
|
296
336
|
end
|
|
297
337
|
|
|
@@ -311,8 +351,6 @@ module Dependabot
|
|
|
311
351
|
|
|
312
352
|
sig { override.void }
|
|
313
353
|
def check_required_files
|
|
314
|
-
return if dependency_files.empty?
|
|
315
|
-
|
|
316
354
|
return if dependency_files.any? { |f| f.name.match?(/^(Julia)?Project\.toml$/i) }
|
|
317
355
|
|
|
318
356
|
raise Dependabot::DependencyFileNotFound, "No Project.toml or JuliaProject.toml found."
|
|
@@ -330,71 +368,81 @@ module Dependabot
|
|
|
330
368
|
|
|
331
369
|
sig { params(content: String, dependency_name: String, new_requirement: String).returns(String) }
|
|
332
370
|
def update_dependency_requirement_in_content(content, dependency_name, new_requirement)
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
#
|
|
340
|
-
pattern = /^(\s*#{Regexp.escape(dependency_name)}\s*=\s*)(?:"[^"]*"|'[^']*'|[^\s#\n]+)(\s*(?:\#.*)?)$/
|
|
341
|
-
|
|
342
|
-
if compat_section.match?(pattern)
|
|
343
|
-
# Replace existing entry in compat section
|
|
344
|
-
updated_compat = compat_section.gsub(pattern, "\\1\"#{new_requirement}\"\\2")
|
|
345
|
-
content.sub(T.must(compat_section_match[0]), "[compat]\n#{updated_compat}")
|
|
346
|
-
else
|
|
347
|
-
# Add new entry to existing [compat] section
|
|
348
|
-
add_compat_entry_to_content(content, dependency_name, new_requirement)
|
|
349
|
-
end
|
|
350
|
-
else
|
|
351
|
-
# Add new [compat] section
|
|
352
|
-
add_compat_entry_to_content(content, dependency_name, new_requirement)
|
|
371
|
+
lines = content.lines
|
|
372
|
+
header_idx = lines.index { |line| line.match?(COMPAT_HEADER_PATTERN) }
|
|
373
|
+
|
|
374
|
+
unless header_idx
|
|
375
|
+
# Add a new [compat] section at the end of the file
|
|
376
|
+
separator = content.end_with?("\n") ? "" : "\n"
|
|
377
|
+
return "#{content}#{separator}\n[compat]\n#{dependency_name} = \"#{new_requirement}\"\n"
|
|
353
378
|
end
|
|
354
|
-
end
|
|
355
379
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
content.sub(T.must(compat_section_match[0]), "[compat]\n#{new_compat_section}")
|
|
369
|
-
else
|
|
370
|
-
content + "\n[compat]\n#{dependency_name} = \"#{requirement}\"\n"
|
|
380
|
+
section_end = ((header_idx + 1)...lines.length).find { |i| T.must(lines[i]).match?(/^\s*\[/) } ||
|
|
381
|
+
lines.length
|
|
382
|
+
|
|
383
|
+
# Replace an existing entry in place, preserving surrounding lines
|
|
384
|
+
entry_pattern =
|
|
385
|
+
/^(\s*#{Regexp.escape(dependency_name)}\s*=\s*)(?:"[^"]*"|'[^']*'|[^\s#\n]+)(\s*(?:\#.*)?)$/
|
|
386
|
+
((header_idx + 1)...section_end).each do |i|
|
|
387
|
+
line = T.must(lines[i])
|
|
388
|
+
next unless line.match?(entry_pattern)
|
|
389
|
+
|
|
390
|
+
lines[i] = line.sub(entry_pattern) { "#{Regexp.last_match(1)}\"#{new_requirement}\"#{Regexp.last_match(2)}" }
|
|
391
|
+
return lines.join
|
|
371
392
|
end
|
|
372
|
-
end
|
|
373
393
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
entries = {}
|
|
377
|
-
compat_section.each_line do |line|
|
|
378
|
-
next if line.strip.empty? || line.strip.start_with?("#")
|
|
394
|
+
insert_compat_entry(lines, header_idx, section_end, dependency_name, new_requirement)
|
|
395
|
+
end
|
|
379
396
|
|
|
380
|
-
|
|
381
|
-
|
|
397
|
+
# Insert a new compat entry in alphabetical position without rewriting
|
|
398
|
+
# the rest of the section, so comments, blank lines and any custom
|
|
399
|
+
# ordering of the existing entries survive.
|
|
400
|
+
sig do
|
|
401
|
+
params(
|
|
402
|
+
lines: T::Array[String],
|
|
403
|
+
header_idx: Integer,
|
|
404
|
+
section_end: Integer,
|
|
405
|
+
dependency_name: String,
|
|
406
|
+
requirement: String
|
|
407
|
+
).returns(String)
|
|
408
|
+
end
|
|
409
|
+
def insert_compat_entry(lines, header_idx, section_end, dependency_name, requirement)
|
|
410
|
+
insert_at = alphabetical_insert_position(lines, header_idx, section_end, dependency_name)
|
|
382
411
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
412
|
+
unless insert_at
|
|
413
|
+
# Append at the end of the section, before any trailing blank lines
|
|
414
|
+
insert_at = section_end
|
|
415
|
+
insert_at -= 1 while insert_at > header_idx + 1 && T.must(lines[insert_at - 1]).strip.empty?
|
|
386
416
|
end
|
|
387
|
-
|
|
417
|
+
|
|
418
|
+
# The preceding line may lack a newline when the section ends the file
|
|
419
|
+
prev = lines[insert_at - 1]
|
|
420
|
+
lines[insert_at - 1] = "#{prev}\n" if prev && !prev.end_with?("\n")
|
|
421
|
+
|
|
422
|
+
lines.insert(insert_at, "#{dependency_name} = \"#{requirement}\"\n")
|
|
423
|
+
lines.join
|
|
388
424
|
end
|
|
389
425
|
|
|
390
|
-
sig
|
|
391
|
-
|
|
392
|
-
|
|
426
|
+
sig do
|
|
427
|
+
params(
|
|
428
|
+
lines: T::Array[String],
|
|
429
|
+
header_idx: Integer,
|
|
430
|
+
section_end: Integer,
|
|
431
|
+
dependency_name: String
|
|
432
|
+
).returns(T.nilable(Integer))
|
|
393
433
|
end
|
|
434
|
+
def alphabetical_insert_position(lines, header_idx, section_end, dependency_name)
|
|
435
|
+
((header_idx + 1)...section_end).each do |i|
|
|
436
|
+
key = T.must(lines[i])[/^\s*([^#\s=][^=\s]*)\s*=/, 1]
|
|
437
|
+
next unless key && key > dependency_name
|
|
438
|
+
|
|
439
|
+
insert_at = i
|
|
440
|
+
# Comment lines directly above an entry belong to it
|
|
441
|
+
insert_at -= 1 while insert_at > header_idx + 1 && T.must(lines[insert_at - 1]).strip.start_with?("#")
|
|
442
|
+
return insert_at
|
|
443
|
+
end
|
|
394
444
|
|
|
395
|
-
|
|
396
|
-
def build_compat_section(entries)
|
|
397
|
-
entries.map { |name, requirement| "#{name} = \"#{requirement}\"\n" }.join
|
|
445
|
+
nil
|
|
398
446
|
end
|
|
399
447
|
end
|
|
400
448
|
end
|
|
@@ -51,34 +51,12 @@ module Dependabot
|
|
|
51
51
|
|
|
52
52
|
sig { params(url_string: String).returns(T.nilable(Dependabot::Source)) }
|
|
53
53
|
def parse_source_url(url_string)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
path_parts = path.split("/")
|
|
61
|
-
return nil if path_parts.length < 2
|
|
62
|
-
|
|
63
|
-
repo = "#{path_parts[0]}/#{path_parts[1]}"
|
|
64
|
-
|
|
65
|
-
# Determine the provider based on hostname
|
|
66
|
-
provider = case hostname
|
|
67
|
-
when "github.com" then "github"
|
|
68
|
-
when "gitlab.com" then "gitlab"
|
|
69
|
-
when /\A.*\.gitlab\.io\z/ then "gitlab"
|
|
70
|
-
else
|
|
71
|
-
Dependabot.logger.info("Unknown SCM provider for #{hostname}, using generic")
|
|
72
|
-
return nil # Return nil for unknown providers
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
Dependabot::Source.new(
|
|
76
|
-
provider: provider,
|
|
77
|
-
repo: repo
|
|
78
|
-
)
|
|
79
|
-
rescue URI::InvalidURIError => e
|
|
80
|
-
Dependabot.logger.error("Invalid URI for dependency #{dependency.name}: #{url_string} - #{e.message}")
|
|
81
|
-
nil
|
|
54
|
+
# Source.from_url understands all providers Dependabot supports
|
|
55
|
+
# (GitHub, GitLab, Bitbucket, Azure DevOps, ...), unlike a
|
|
56
|
+
# hand-rolled hostname switch
|
|
57
|
+
source = Dependabot::Source.from_url(url_string)
|
|
58
|
+
Dependabot.logger.info("Unknown SCM provider for #{url_string}") if source.nil?
|
|
59
|
+
source
|
|
82
60
|
end
|
|
83
61
|
end
|
|
84
62
|
end
|
|
@@ -39,27 +39,23 @@ module Dependabot
|
|
|
39
39
|
|
|
40
40
|
sig { returns(T::Array[Dependabot::Package::PackageRelease]) }
|
|
41
41
|
def fetch_package_releases
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
rescue StandardError => e
|
|
60
|
-
Dependabot.logger.error("Error while fetching package releases for #{dependency.name}: #{e.message}")
|
|
61
|
-
releases
|
|
62
|
-
end
|
|
42
|
+
registry_client = RegistryClient.new(
|
|
43
|
+
credentials: credentials,
|
|
44
|
+
custom_registries: custom_registries
|
|
45
|
+
)
|
|
46
|
+
uuid = T.cast(dependency.metadata[:julia_uuid], T.nilable(String))
|
|
47
|
+
|
|
48
|
+
# Fetch all available versions. Domain errors (package not found)
|
|
49
|
+
# come back as an empty list; infrastructure failures raise and are
|
|
50
|
+
# classified by the updater's error handler rather than being
|
|
51
|
+
# silently treated as "no update available".
|
|
52
|
+
available_versions = registry_client.fetch_available_versions(dependency.name, uuid)
|
|
53
|
+
return [] if available_versions.empty?
|
|
54
|
+
|
|
55
|
+
releases = build_releases_for_versions(registry_client, available_versions, uuid)
|
|
56
|
+
mark_latest_release(releases)
|
|
57
|
+
|
|
58
|
+
releases
|
|
63
59
|
end
|
|
64
60
|
|
|
65
61
|
private
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
# frozen_string_literal: true
|
|
4
4
|
|
|
5
5
|
require "time"
|
|
6
|
+
require "uri"
|
|
7
|
+
require "fileutils"
|
|
8
|
+
require "toml-rb"
|
|
6
9
|
require "dependabot/credential"
|
|
7
10
|
require "dependabot/julia/version"
|
|
8
11
|
require "dependabot/shared_helpers"
|
|
@@ -35,18 +38,18 @@ module Dependabot
|
|
|
35
38
|
)
|
|
36
39
|
|
|
37
40
|
# Check if the result itself contains an error (package not found)
|
|
38
|
-
|
|
41
|
+
if result["error"]
|
|
42
|
+
Dependabot.logger.warn(
|
|
43
|
+
"Failed to fetch latest version for #{package_name}: #{result['error']}"
|
|
44
|
+
)
|
|
45
|
+
return nil
|
|
46
|
+
end
|
|
39
47
|
|
|
40
48
|
# Extract version from the result structure
|
|
41
49
|
# The Julia helper returns version directly in the result
|
|
42
50
|
return nil unless result["version"]
|
|
43
51
|
|
|
44
52
|
Gem::Version.new(result["version"])
|
|
45
|
-
rescue StandardError => e
|
|
46
|
-
Dependabot.logger.warn(
|
|
47
|
-
"Failed to fetch latest version for #{package_name}: #{e.message}"
|
|
48
|
-
)
|
|
49
|
-
nil
|
|
50
53
|
end
|
|
51
54
|
|
|
52
55
|
sig { params(package_name: String, package_uuid: T.nilable(String)).returns(T.nilable(Gem::Version)) }
|
|
@@ -63,17 +66,17 @@ module Dependabot
|
|
|
63
66
|
)
|
|
64
67
|
|
|
65
68
|
# Check if the result itself contains an error (package not found)
|
|
66
|
-
|
|
69
|
+
if result["error"]
|
|
70
|
+
Dependabot.logger.warn(
|
|
71
|
+
"Failed to fetch latest version with custom registries for #{package_name}: #{result['error']}"
|
|
72
|
+
)
|
|
73
|
+
return nil
|
|
74
|
+
end
|
|
67
75
|
|
|
68
76
|
# Extract version from the result structure
|
|
69
77
|
return nil unless result["version"]
|
|
70
78
|
|
|
71
79
|
Gem::Version.new(result["version"])
|
|
72
|
-
rescue StandardError => e
|
|
73
|
-
Dependabot.logger.warn(
|
|
74
|
-
"Failed to fetch latest version with custom registries for #{package_name}: #{e.message}"
|
|
75
|
-
)
|
|
76
|
-
nil
|
|
77
80
|
end
|
|
78
81
|
|
|
79
82
|
sig { params(package_name: String, package_uuid: String).returns(T.nilable(T::Hash[String, T.untyped])) }
|
|
@@ -82,9 +85,6 @@ module Dependabot
|
|
|
82
85
|
function: "get_package_metadata",
|
|
83
86
|
args: { package_name: package_name, package_uuid: package_uuid }
|
|
84
87
|
)
|
|
85
|
-
rescue StandardError => e
|
|
86
|
-
Dependabot.logger.warn("Failed to fetch metadata for #{package_name}: #{e.message}")
|
|
87
|
-
nil
|
|
88
88
|
end
|
|
89
89
|
|
|
90
90
|
sig do
|
|
@@ -124,9 +124,6 @@ module Dependabot
|
|
|
124
124
|
"project_file" => result["project_file"],
|
|
125
125
|
"manifest_file" => result["manifest_file"]
|
|
126
126
|
}
|
|
127
|
-
rescue StandardError => e
|
|
128
|
-
Dependabot.logger.warn("Failed to find environment files in #{directory}: #{e.message}")
|
|
129
|
-
{}
|
|
130
127
|
end
|
|
131
128
|
|
|
132
129
|
sig { params(directory: String).returns(T::Hash[String, T.untyped]) }
|
|
@@ -143,9 +140,6 @@ module Dependabot
|
|
|
143
140
|
"manifest_file" => result["manifest_file"] || "",
|
|
144
141
|
"workspace_root" => result["workspace_root"] || ""
|
|
145
142
|
}
|
|
146
|
-
rescue StandardError => e
|
|
147
|
-
Dependabot.logger.warn("Failed to find workspace project files in #{directory}: #{e.message}")
|
|
148
|
-
{ "error" => e.message }
|
|
149
143
|
end
|
|
150
144
|
|
|
151
145
|
sig do
|
|
@@ -182,7 +176,7 @@ module Dependabot
|
|
|
182
176
|
sig do
|
|
183
177
|
params(
|
|
184
178
|
project_path: String,
|
|
185
|
-
updates: T::Hash[String, String]
|
|
179
|
+
updates: T::Hash[String, T::Hash[String, String]]
|
|
186
180
|
).returns(T::Hash[String, T.untyped])
|
|
187
181
|
end
|
|
188
182
|
def update_manifest(project_path:, updates:)
|
|
@@ -213,8 +207,8 @@ module Dependabot
|
|
|
213
207
|
return nil unless result["release_date"]
|
|
214
208
|
|
|
215
209
|
Time.parse(result["release_date"])
|
|
216
|
-
rescue
|
|
217
|
-
Dependabot.logger.warn("Failed to
|
|
210
|
+
rescue ArgumentError => e
|
|
211
|
+
Dependabot.logger.warn("Failed to parse release date for #{package_name} v#{version}: #{e.message}")
|
|
218
212
|
nil
|
|
219
213
|
end
|
|
220
214
|
|
|
@@ -232,16 +226,16 @@ module Dependabot
|
|
|
232
226
|
)
|
|
233
227
|
|
|
234
228
|
# Check if the result contains an error
|
|
235
|
-
|
|
229
|
+
if result["error"]
|
|
230
|
+
Dependabot.logger.warn("Failed to fetch available versions for #{package_name}: #{result['error']}")
|
|
231
|
+
return []
|
|
232
|
+
end
|
|
236
233
|
|
|
237
234
|
# Extract versions array from the result
|
|
238
235
|
versions = result["versions"]
|
|
239
236
|
return [] unless versions.is_a?(Array)
|
|
240
237
|
|
|
241
238
|
versions.map(&:to_s)
|
|
242
|
-
rescue StandardError => e
|
|
243
|
-
Dependabot.logger.warn("Failed to fetch available versions for #{package_name}: #{e.message}")
|
|
244
|
-
[]
|
|
245
239
|
end
|
|
246
240
|
|
|
247
241
|
sig { params(package_name: String, package_uuid: T.nilable(String)).returns(T::Array[String]) }
|
|
@@ -258,18 +252,18 @@ module Dependabot
|
|
|
258
252
|
)
|
|
259
253
|
|
|
260
254
|
# Check if the result contains an error
|
|
261
|
-
|
|
255
|
+
if result["error"]
|
|
256
|
+
Dependabot.logger.warn(
|
|
257
|
+
"Failed to fetch available versions with custom registries for #{package_name}: #{result['error']}"
|
|
258
|
+
)
|
|
259
|
+
return []
|
|
260
|
+
end
|
|
262
261
|
|
|
263
262
|
# Extract versions array from the result
|
|
264
263
|
versions = result["versions"]
|
|
265
264
|
return [] unless versions.is_a?(Array)
|
|
266
265
|
|
|
267
266
|
versions.map(&:to_s)
|
|
268
|
-
rescue StandardError => e
|
|
269
|
-
Dependabot.logger.warn(
|
|
270
|
-
"Failed to fetch available versions with custom registries for #{package_name}: #{e.message}"
|
|
271
|
-
)
|
|
272
|
-
[]
|
|
273
267
|
end
|
|
274
268
|
|
|
275
269
|
# ============================================================================
|
|
@@ -278,6 +272,8 @@ module Dependabot
|
|
|
278
272
|
|
|
279
273
|
sig { params(dependencies: T::Array[Dependabot::Dependency]).returns(T::Hash[String, T.untyped]) }
|
|
280
274
|
def batch_fetch_package_info(dependencies)
|
|
275
|
+
return {} if dependencies.empty?
|
|
276
|
+
|
|
281
277
|
packages = dependencies.map do |dep|
|
|
282
278
|
{
|
|
283
279
|
name: dep.name,
|
|
@@ -289,9 +285,6 @@ module Dependabot
|
|
|
289
285
|
function: "batch_get_package_info",
|
|
290
286
|
args: { packages: packages }
|
|
291
287
|
)
|
|
292
|
-
rescue StandardError => e
|
|
293
|
-
Dependabot.logger.error("Failed to batch fetch package info: #{e.message}")
|
|
294
|
-
{}
|
|
295
288
|
end
|
|
296
289
|
|
|
297
290
|
sig do
|
|
@@ -300,6 +293,8 @@ module Dependabot
|
|
|
300
293
|
).returns(T::Hash[String, T::Hash[String, T.nilable(String)]])
|
|
301
294
|
end
|
|
302
295
|
def batch_fetch_version_release_dates(packages_versions)
|
|
296
|
+
return {} if packages_versions.empty?
|
|
297
|
+
|
|
303
298
|
result = call_julia_helper(
|
|
304
299
|
function: "batch_get_version_release_dates",
|
|
305
300
|
args: { packages_versions: packages_versions }
|
|
@@ -311,13 +306,12 @@ module Dependabot
|
|
|
311
306
|
|
|
312
307
|
dates.is_a?(Hash) ? dates : {}
|
|
313
308
|
end
|
|
314
|
-
rescue StandardError => e
|
|
315
|
-
Dependabot.logger.error("Failed to batch fetch version release dates: #{e.message}")
|
|
316
|
-
{}
|
|
317
309
|
end
|
|
318
310
|
|
|
319
311
|
sig { params(dependencies: T::Array[Dependabot::Dependency]).returns(T::Hash[String, T.untyped]) }
|
|
320
312
|
def batch_fetch_available_versions(dependencies)
|
|
313
|
+
return {} if dependencies.empty?
|
|
314
|
+
|
|
321
315
|
packages = dependencies.map do |dep|
|
|
322
316
|
{
|
|
323
317
|
name: dep.name,
|
|
@@ -329,9 +323,6 @@ module Dependabot
|
|
|
329
323
|
function: "batch_get_available_versions",
|
|
330
324
|
args: { packages: packages }
|
|
331
325
|
)
|
|
332
|
-
rescue StandardError => e
|
|
333
|
-
Dependabot.logger.error("Failed to batch fetch available versions: #{e.message}")
|
|
334
|
-
{}
|
|
335
326
|
end
|
|
336
327
|
|
|
337
328
|
private
|
|
@@ -385,21 +376,59 @@ module Dependabot
|
|
|
385
376
|
|
|
386
377
|
if ENV["DEPENDABOT_NATIVE_HELPERS_PATH"]
|
|
387
378
|
# In production/CI, use the shared depot where packages were precompiled
|
|
388
|
-
user_depot = File.join(ENV.fetch("HOME", "/home/dependabot"), ".julia")
|
|
389
379
|
# Trailing : is intentional. It automatically includes the bundled stdlibs
|
|
390
|
-
env["JULIA_DEPOT_PATH"] = "#{
|
|
380
|
+
env["JULIA_DEPOT_PATH"] = "#{julia_user_depot}:"
|
|
391
381
|
end
|
|
392
382
|
# In development use the default Julia depot
|
|
393
383
|
|
|
394
|
-
#
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
env["
|
|
384
|
+
# Pkg supports a single package server via JULIA_PKG_SERVER;
|
|
385
|
+
# authentication uses an auth.toml in the depot, not env vars.
|
|
386
|
+
pkg_server = pkg_server_credential
|
|
387
|
+
if pkg_server
|
|
388
|
+
env["JULIA_PKG_SERVER"] = pkg_server.fetch("url")
|
|
389
|
+
configure_pkg_server_auth(pkg_server)
|
|
399
390
|
end
|
|
400
391
|
|
|
401
392
|
env
|
|
402
393
|
end
|
|
394
|
+
|
|
395
|
+
sig { returns(T.nilable(Dependabot::Credential)) }
|
|
396
|
+
def pkg_server_credential
|
|
397
|
+
julia_credentials = credentials.select { |c| c["type"] == "julia_registry" && c["url"] }
|
|
398
|
+
if julia_credentials.length > 1
|
|
399
|
+
Dependabot.logger.warn(
|
|
400
|
+
"Multiple julia_registry credentials configured; Julia's Pkg supports a single " \
|
|
401
|
+
"package server, using the first"
|
|
402
|
+
)
|
|
403
|
+
end
|
|
404
|
+
julia_credentials.first
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# Write the package server token where Pkg actually reads it:
|
|
408
|
+
# <depot>/servers/<host>/auth.toml
|
|
409
|
+
sig { params(credential: Dependabot::Credential).void }
|
|
410
|
+
def configure_pkg_server_auth(credential)
|
|
411
|
+
token = credential["token"]
|
|
412
|
+
return unless token
|
|
413
|
+
|
|
414
|
+
host = URI.parse(credential.fetch("url")).host
|
|
415
|
+
return unless host
|
|
416
|
+
|
|
417
|
+
auth_dir = File.join(julia_user_depot, "servers", host)
|
|
418
|
+
FileUtils.mkdir_p(auth_dir)
|
|
419
|
+
File.write(File.join(auth_dir, "auth.toml"), TomlRB.dump({ "access_token" => token }))
|
|
420
|
+
rescue URI::InvalidURIError => e
|
|
421
|
+
Dependabot.logger.warn("Invalid julia_registry URL: #{e.message}")
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
sig { returns(String) }
|
|
425
|
+
def julia_user_depot
|
|
426
|
+
if ENV["DEPENDABOT_NATIVE_HELPERS_PATH"]
|
|
427
|
+
File.join(ENV.fetch("HOME", "/home/dependabot"), ".julia")
|
|
428
|
+
else
|
|
429
|
+
File.join(Dir.home, ".julia")
|
|
430
|
+
end
|
|
431
|
+
end
|
|
403
432
|
end
|
|
404
433
|
end
|
|
405
434
|
end
|
|
@@ -11,7 +11,7 @@ module Dependabot
|
|
|
11
11
|
def self.requirements_array(requirement_string)
|
|
12
12
|
# Julia version specifiers can be:
|
|
13
13
|
# - Exact: "1.2.3"
|
|
14
|
-
# - Range: "1.2-1.3", ">=1.0, <2.0"
|
|
14
|
+
# - Range: "1.2 - 1.3", ">=1.0, <2.0"
|
|
15
15
|
# - Caret: "^1.2" (compatible within major version)
|
|
16
16
|
# - Tilde: "~1.2.3" (compatible within minor version)
|
|
17
17
|
# Note: Missing compat entry (nil/empty) means any version is acceptable
|
|
@@ -35,6 +35,13 @@ module Dependabot
|
|
|
35
35
|
# Separate constraints (e.g., "^1.10, 2" or "0.34, 0.35") use version specs
|
|
36
36
|
# (with or without ^/~) as OR conditions - any matching spec is acceptable.
|
|
37
37
|
# Only treat as compound if ALL constraints use explicit comparison operators.
|
|
38
|
+
#
|
|
39
|
+
# NOTE: Julia's Pkg unions *all* comma-separated compat specs, including
|
|
40
|
+
# operator-style ones, so ">= 1.0, < 2.0" is `*` to the resolver. We keep
|
|
41
|
+
# intersection semantics for operator-style lists anyway because this
|
|
42
|
+
# method also parses Dependabot ignore conditions (e.g. ">= 2.a, < 3"),
|
|
43
|
+
# which are always intersections; treating those as unions would make
|
|
44
|
+
# every ignore condition match all versions.
|
|
38
45
|
return false if constraints.length <= 1
|
|
39
46
|
|
|
40
47
|
constraints.all? { |c| c.match?(/^[<>=]/) }
|
|
@@ -65,11 +72,15 @@ module Dependabot
|
|
|
65
72
|
version
|
|
66
73
|
end
|
|
67
74
|
|
|
75
|
+
# Julia hyphen ranges require whitespace around the hyphen ("1.2 - 3.4");
|
|
76
|
+
# without spaces the hyphen introduces a prerelease tag instead.
|
|
77
|
+
HYPHEN_RANGE_PATTERN = T.let(/^(\d+(?:\.\d+)*)\s+-\s+(\d+(?:\.\d+)*)$/, Regexp)
|
|
78
|
+
|
|
68
79
|
sig { params(constraint: String).returns(T::Array[String]) }
|
|
69
80
|
def self.normalize_julia_constraint(constraint)
|
|
70
81
|
return normalize_caret_constraint(constraint) if constraint.match?(/^\^(\d+(?:\.\d+)*)/)
|
|
71
82
|
return normalize_tilde_constraint(constraint) if constraint.match?(/^~(\d+(?:\.\d+)*)/)
|
|
72
|
-
return normalize_range_constraint(constraint) if constraint.match?(
|
|
83
|
+
return normalize_range_constraint(constraint) if constraint.match?(HYPHEN_RANGE_PATTERN)
|
|
73
84
|
|
|
74
85
|
# Julia treats plain version numbers as caret constraints (implicit ^)
|
|
75
86
|
# e.g., "1.2.3" is equivalent to "^1.2.3" which means ">= 1.2.3, < 2.0.0"
|
|
@@ -134,18 +145,21 @@ module Dependabot
|
|
|
134
145
|
|
|
135
146
|
sig { params(constraint: String).returns(T::Array[String]) }
|
|
136
147
|
private_class_method def self.normalize_range_constraint(constraint)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
148
|
+
match = T.must(constraint.match(HYPHEN_RANGE_PATTERN))
|
|
149
|
+
start_version = T.must(match[1])
|
|
150
|
+
end_version = T.must(match[2])
|
|
151
|
+
end_parts = end_version.split(".")
|
|
152
|
+
|
|
153
|
+
# Julia hyphen-range semantics (https://pkgdocs.julialang.org/v1/compatibility/):
|
|
154
|
+
# a fully-specified end is inclusive ("1.2.3 - 4.5.6" admits exactly 4.5.6),
|
|
155
|
+
# while a partial end acts as a wildcard ("0.2 - 0.5" means up to 0.5.*).
|
|
156
|
+
upper_bound = case end_parts.length
|
|
157
|
+
when 1 then "< #{T.must(end_parts[0]).to_i + 1}.0.0"
|
|
158
|
+
when 2 then "< #{T.must(end_parts[0])}.#{T.must(end_parts[1]).to_i + 1}.0"
|
|
159
|
+
else "<= #{end_version}"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
[">= #{start_version}", upper_bound]
|
|
149
163
|
end
|
|
150
164
|
end
|
|
151
165
|
end
|
|
@@ -19,7 +19,7 @@ module Dependabot
|
|
|
19
19
|
ignored_versions: T::Array[String],
|
|
20
20
|
security_advisories: T::Array[Dependabot::SecurityAdvisory],
|
|
21
21
|
raise_on_ignored: T::Boolean,
|
|
22
|
-
cooldown_config: T.nilable(T::Hash[Symbol,
|
|
22
|
+
cooldown_config: T.nilable(T::Hash[Symbol, Object]),
|
|
23
23
|
custom_registries: T::Array[T::Hash[Symbol, String]]
|
|
24
24
|
).void
|
|
25
25
|
end
|
|
@@ -45,7 +45,14 @@ module Dependabot
|
|
|
45
45
|
|
|
46
46
|
sig { returns(T.nilable(Gem::Version)) }
|
|
47
47
|
def latest_version
|
|
48
|
-
@latest_version ||= T.let(
|
|
48
|
+
@latest_version ||= T.let(available_versions.max, T.nilable(Gem::Version))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# All selectable versions after cooldown, ignored-version and
|
|
52
|
+
# vulnerability filtering, in ascending order.
|
|
53
|
+
sig { returns(T::Array[Gem::Version]) }
|
|
54
|
+
def available_versions
|
|
55
|
+
@available_versions ||= T.let(fetch_available_versions, T.nilable(T::Array[Gem::Version]))
|
|
49
56
|
end
|
|
50
57
|
|
|
51
58
|
private
|
|
@@ -68,14 +75,14 @@ module Dependabot
|
|
|
68
75
|
sig { returns(T::Boolean) }
|
|
69
76
|
attr_reader :raise_on_ignored
|
|
70
77
|
|
|
71
|
-
sig { returns(T.nilable(T::Hash[Symbol,
|
|
78
|
+
sig { returns(T.nilable(T::Hash[Symbol, Object])) }
|
|
72
79
|
attr_reader :cooldown_config
|
|
73
80
|
|
|
74
81
|
sig { returns(T::Array[T::Hash[Symbol, String]]) }
|
|
75
82
|
attr_reader :custom_registries
|
|
76
83
|
|
|
77
|
-
sig { returns(T
|
|
78
|
-
def
|
|
84
|
+
sig { returns(T::Array[Gem::Version]) }
|
|
85
|
+
def fetch_available_versions
|
|
79
86
|
# Fetch all package releases using the PackageDetailsFetcher
|
|
80
87
|
package_fetcher = Julia::Package::PackageDetailsFetcher.new(
|
|
81
88
|
dependency: dependency,
|
|
@@ -84,32 +91,34 @@ module Dependabot
|
|
|
84
91
|
)
|
|
85
92
|
|
|
86
93
|
releases = package_fetcher.fetch_package_releases
|
|
87
|
-
return
|
|
94
|
+
return [] if releases.empty?
|
|
88
95
|
|
|
89
96
|
# Filter releases based on cooldown
|
|
90
97
|
if cooldown_config
|
|
91
98
|
releases = filter_releases_by_cooldown(releases)
|
|
92
|
-
return
|
|
99
|
+
return [] if releases.empty?
|
|
93
100
|
end
|
|
94
101
|
|
|
95
102
|
# Convert to versions for further filtering
|
|
96
103
|
versions = releases.map(&:version).sort
|
|
97
104
|
|
|
105
|
+
# Filter out prereleases unless the dependency is already on one
|
|
106
|
+
versions = filter_prerelease_versions(versions)
|
|
107
|
+
return [] if versions.empty?
|
|
108
|
+
|
|
98
109
|
# Filter out ignored versions
|
|
99
110
|
versions = filter_ignored_versions(versions)
|
|
100
|
-
return
|
|
111
|
+
return [] if versions.empty?
|
|
101
112
|
|
|
102
113
|
# Filter out lower versions
|
|
103
114
|
versions = filter_lower_versions(versions)
|
|
104
|
-
return
|
|
115
|
+
return [] if versions.empty?
|
|
105
116
|
|
|
106
117
|
# Filter out vulnerable versions
|
|
107
|
-
|
|
118
|
+
Dependabot::UpdateCheckers::VersionFilters.filter_vulnerable_versions(
|
|
108
119
|
versions,
|
|
109
120
|
security_advisories
|
|
110
121
|
)
|
|
111
|
-
|
|
112
|
-
filtered_versions.max
|
|
113
122
|
end
|
|
114
123
|
|
|
115
124
|
sig do
|
|
@@ -126,6 +135,21 @@ module Dependabot
|
|
|
126
135
|
end
|
|
127
136
|
end
|
|
128
137
|
|
|
138
|
+
sig { params(versions: T::Array[Gem::Version]).returns(T::Array[Gem::Version]) }
|
|
139
|
+
def filter_prerelease_versions(versions)
|
|
140
|
+
return versions if wants_prerelease?
|
|
141
|
+
|
|
142
|
+
versions.reject(&:prerelease?)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
sig { returns(T::Boolean) }
|
|
146
|
+
def wants_prerelease?
|
|
147
|
+
version = dependency.version
|
|
148
|
+
return false unless version
|
|
149
|
+
|
|
150
|
+
Dependabot::Julia::Version.new(version).prerelease?
|
|
151
|
+
end
|
|
152
|
+
|
|
129
153
|
sig { params(versions: T::Array[Gem::Version]).returns(T::Array[Gem::Version]) }
|
|
130
154
|
def filter_ignored_versions(versions)
|
|
131
155
|
filtered = versions.reject do |version|
|
|
@@ -148,7 +172,7 @@ module Dependabot
|
|
|
148
172
|
def filter_lower_versions(versions)
|
|
149
173
|
return versions unless dependency.version
|
|
150
174
|
|
|
151
|
-
current_version =
|
|
175
|
+
current_version = Dependabot::Julia::Version.new(dependency.version)
|
|
152
176
|
versions.select { |v| v > current_version }
|
|
153
177
|
end
|
|
154
178
|
|
|
@@ -180,19 +204,27 @@ module Dependabot
|
|
|
180
204
|
excludes = T.cast(config[:exclude], T.nilable(T::Array[String]))
|
|
181
205
|
|
|
182
206
|
# Check exclusions first
|
|
183
|
-
return false if excludes&.any? { |pattern| dependency.name.match?(
|
|
207
|
+
return false if excludes&.any? { |pattern| dependency.name.match?(cooldown_pattern_regex(pattern)) }
|
|
184
208
|
|
|
185
209
|
# Check inclusions (if specified, dependency must match)
|
|
186
|
-
return includes.any? { |pattern| dependency.name.match?(
|
|
210
|
+
return includes.any? { |pattern| dependency.name.match?(cooldown_pattern_regex(pattern)) } if includes&.any?
|
|
187
211
|
|
|
188
212
|
true # Include by default if no include patterns specified
|
|
189
213
|
end
|
|
190
214
|
|
|
215
|
+
# Cooldown include/exclude entries are shell-style globs where only "*"
|
|
216
|
+
# is a wildcard; everything else matches literally and the whole name
|
|
217
|
+
# must match (so "JSON" doesn't also cover "JSON3" or "LazyJSON").
|
|
218
|
+
sig { params(pattern: String).returns(Regexp) }
|
|
219
|
+
def cooldown_pattern_regex(pattern)
|
|
220
|
+
Regexp.new("\\A#{pattern.split('*', -1).map { |part| Regexp.escape(part) }.join('.*')}\\z")
|
|
221
|
+
end
|
|
222
|
+
|
|
191
223
|
sig { params(version: Gem::Version).returns(T.nilable(Integer)) }
|
|
192
224
|
def determine_cooldown_days(version)
|
|
193
225
|
return nil unless cooldown_config
|
|
194
226
|
|
|
195
|
-
current_version = dependency.version ?
|
|
227
|
+
current_version = dependency.version ? Dependabot::Julia::Version.new(dependency.version) : nil
|
|
196
228
|
return nil unless current_version
|
|
197
229
|
|
|
198
230
|
version_bump_type = determine_version_bump_type(version, current_version)
|
|
@@ -23,12 +23,15 @@ module Dependabot
|
|
|
23
23
|
T::Array[Dependabot::DependencyRequirement]
|
|
24
24
|
)
|
|
25
25
|
@target_version = target_version
|
|
26
|
-
|
|
26
|
+
# Julia's ecosystem convention (CompatHelper) is to append a new spec
|
|
27
|
+
# to the existing compat entry, i.e. widen, so that is the default.
|
|
28
|
+
@update_strategy = T.let(update_strategy || :widen_ranges, Symbol)
|
|
27
29
|
end
|
|
28
30
|
|
|
29
31
|
sig { returns(T::Array[Dependabot::DependencyRequirement]) }
|
|
30
32
|
def updated_requirements
|
|
31
33
|
return requirements unless target_version
|
|
34
|
+
return requirements if update_strategy == :lockfile_only
|
|
32
35
|
|
|
33
36
|
target_version_obj = Dependabot::Julia::Version.new(target_version)
|
|
34
37
|
|
|
@@ -69,17 +72,29 @@ module Dependabot
|
|
|
69
72
|
|
|
70
73
|
sig { params(requirement_string: String, target_version: Dependabot::Julia::Version).returns(String) }
|
|
71
74
|
def updated_version_requirement(requirement_string, target_version)
|
|
72
|
-
# Don't update range requirements (e.g., "0.34-0.35") - these are explicit manual constraints
|
|
73
|
-
return requirement_string if requirement_string.match?(
|
|
75
|
+
# Don't update range requirements (e.g., "0.34 - 0.35") - these are explicit manual constraints
|
|
76
|
+
return requirement_string if requirement_string.match?(Dependabot::Julia::Requirement::HYPHEN_RANGE_PATTERN)
|
|
74
77
|
|
|
75
78
|
# Parse all constraints in the requirement string
|
|
76
79
|
reqs = Dependabot::Julia::Requirement.requirements_array(requirement_string)
|
|
77
80
|
|
|
78
81
|
# Check if any requirement is satisfied by the target version
|
|
79
82
|
# Note: This uses the implicit caret semantics from the Requirement class
|
|
80
|
-
|
|
83
|
+
satisfied = reqs.any? { |req| req.satisfied_by?(target_version) }
|
|
84
|
+
|
|
85
|
+
case update_strategy
|
|
86
|
+
when :bump_versions
|
|
87
|
+
simplified_version_spec(target_version)
|
|
88
|
+
when :bump_versions_if_necessary
|
|
89
|
+
satisfied ? requirement_string : simplified_version_spec(target_version)
|
|
90
|
+
else # :widen_ranges
|
|
91
|
+
satisfied ? requirement_string : append_spec(requirement_string, target_version)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
81
94
|
|
|
82
|
-
|
|
95
|
+
sig { params(requirement_string: String, target_version: Dependabot::Julia::Version).returns(String) }
|
|
96
|
+
def append_spec(requirement_string, target_version)
|
|
97
|
+
# Append a new requirement that includes the target version
|
|
83
98
|
# Following CompatHelper.jl's approach: use major.minor for versions >= 1.0,
|
|
84
99
|
# 0.minor for 0.x versions, and 0.0.patch for 0.0.x versions
|
|
85
100
|
new_spec = simplified_version_spec(target_version)
|
|
@@ -77,19 +77,24 @@ module Dependabot
|
|
|
77
77
|
|
|
78
78
|
sig { override.returns(T.nilable(T.any(Dependabot::Version, String))) }
|
|
79
79
|
def latest_resolvable_version_with_no_unlock
|
|
80
|
-
# Return
|
|
81
|
-
|
|
80
|
+
# Return the highest available version that satisfies the current
|
|
81
|
+
# requirement constraints. requirements_array applies Julia compat
|
|
82
|
+
# semantics (implicit caret, tilde, hyphen ranges) and returns
|
|
83
|
+
# alternatives that are OR'd together.
|
|
84
|
+
candidates = latest_version_finder.available_versions
|
|
85
|
+
return nil if candidates.empty?
|
|
82
86
|
|
|
83
87
|
current_requirement = T.cast(dependency.requirements.first&.fetch(:requirement, nil), T.nilable(String))
|
|
84
88
|
|
|
85
|
-
if current_requirement.nil? || current_requirement == "*"
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
best = if current_requirement.nil? || current_requirement.strip == "*"
|
|
90
|
+
candidates.max
|
|
91
|
+
else
|
|
92
|
+
reqs = requirement_class.requirements_array(current_requirement)
|
|
93
|
+
candidates.select { |version| reqs.any? { |req| req.satisfied_by?(version) } }.max
|
|
94
|
+
end
|
|
95
|
+
return nil unless best
|
|
91
96
|
|
|
92
|
-
Dependabot::Julia::Version.new(
|
|
97
|
+
Dependabot::Julia::Version.new(best.to_s)
|
|
93
98
|
end
|
|
94
99
|
|
|
95
100
|
sig { override.returns(T::Array[Dependabot::DependencyRequirement]) }
|
|
@@ -131,8 +136,9 @@ module Dependabot
|
|
|
131
136
|
semver_major_days: cooldown.semver_major_days,
|
|
132
137
|
semver_minor_days: cooldown.semver_minor_days,
|
|
133
138
|
semver_patch_days: cooldown.semver_patch_days,
|
|
134
|
-
|
|
135
|
-
|
|
139
|
+
# ReleaseCooldownOptions stores these as Sets; LatestVersionFinder expects Arrays.
|
|
140
|
+
include: cooldown.include.to_a,
|
|
141
|
+
exclude: cooldown.exclude.to_a
|
|
136
142
|
}
|
|
137
143
|
end
|
|
138
144
|
|
|
@@ -6,16 +6,21 @@ require "dependabot/version"
|
|
|
6
6
|
module Dependabot
|
|
7
7
|
module Julia
|
|
8
8
|
class Version < Dependabot::Version
|
|
9
|
-
# Julia follows semantic versioning
|
|
9
|
+
# Julia follows semantic versioning, including prerelease tags and build
|
|
10
|
+
# metadata. Build metadata is significant in Julia: JLL packages register
|
|
11
|
+
# versions like "1.6.10+0", "1.6.10+1" where the build number orders new
|
|
12
|
+
# builds of the same upstream version.
|
|
10
13
|
# See: https://docs.julialang.org/en/v1/stdlib/Pkg/#Version-specifier-format
|
|
11
|
-
VERSION_PATTERN = T.let(
|
|
14
|
+
VERSION_PATTERN = T.let(
|
|
15
|
+
/^v?\d+(?:\.\d+)*(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/,
|
|
16
|
+
Regexp
|
|
17
|
+
)
|
|
12
18
|
|
|
13
19
|
sig { override.params(version: T.nilable(T.any(String, Integer, Gem::Version))).returns(T::Boolean) }
|
|
14
20
|
def self.correct?(version)
|
|
15
21
|
return false if version.nil?
|
|
16
22
|
|
|
17
|
-
|
|
18
|
-
VERSION_PATTERN.match?(version_string)
|
|
23
|
+
VERSION_PATTERN.match?(version.to_s.strip)
|
|
19
24
|
end
|
|
20
25
|
|
|
21
26
|
sig { override.params(version: T.nilable(T.any(String, Integer, Gem::Version))).void }
|
|
@@ -26,7 +31,7 @@ module Dependabot
|
|
|
26
31
|
version_string = version_string.sub(/^v/, "") if version_string.match?(/^v\d/)
|
|
27
32
|
|
|
28
33
|
@version_string = T.let(version_string, String)
|
|
29
|
-
super(version_string)
|
|
34
|
+
super(self.class.gem_compatible_version_string(version_string))
|
|
30
35
|
end
|
|
31
36
|
|
|
32
37
|
sig do
|
|
@@ -37,6 +42,29 @@ module Dependabot
|
|
|
37
42
|
def self.new(version)
|
|
38
43
|
T.cast(super, Dependabot::Julia::Version)
|
|
39
44
|
end
|
|
45
|
+
|
|
46
|
+
# Gem::Version rejects "+" so build metadata has to be folded away before
|
|
47
|
+
# calling super. A numeric build becomes an extra segment, which preserves
|
|
48
|
+
# Julia's ordering between builds ("1.6.10+1" > "1.6.10+0") and against
|
|
49
|
+
# neighbouring versions ("1.6.10+1" < "1.6.11"). Non-numeric builds are
|
|
50
|
+
# dropped for comparison purposes.
|
|
51
|
+
sig { params(version_string: String).returns(String) }
|
|
52
|
+
def self.gem_compatible_version_string(version_string)
|
|
53
|
+
base, build = version_string.split("+", 2)
|
|
54
|
+
return version_string unless build
|
|
55
|
+
|
|
56
|
+
build.match?(/\A\d+\z/) ? "#{base}.#{build}" : T.must(base)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
sig { returns(String) }
|
|
60
|
+
def to_s
|
|
61
|
+
@version_string
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
sig { override.returns(String) }
|
|
65
|
+
def to_semver
|
|
66
|
+
@version_string
|
|
67
|
+
end
|
|
40
68
|
end
|
|
41
69
|
end
|
|
42
70
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dependabot-julia
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.387.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dependabot
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - '='
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.
|
|
18
|
+
version: 0.387.0
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - '='
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.
|
|
25
|
+
version: 0.387.0
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: debug
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -261,7 +261,7 @@ licenses:
|
|
|
261
261
|
- MIT
|
|
262
262
|
metadata:
|
|
263
263
|
bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
|
|
264
|
-
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.
|
|
264
|
+
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.387.0
|
|
265
265
|
rdoc_options: []
|
|
266
266
|
require_paths:
|
|
267
267
|
- lib
|