dependabot-julia 0.345.0 → 0.346.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 +40 -25
- data/lib/dependabot/julia/file_parser.rb +27 -139
- data/lib/dependabot/julia/file_updater.rb +213 -171
- data/lib/dependabot/julia/metadata_finder.rb +2 -14
- data/lib/dependabot/julia/package/package_details_fetcher.rb +41 -15
- data/lib/dependabot/julia/package_manager.rb +2 -2
- data/lib/dependabot/julia/registry_client.rb +80 -34
- data/lib/dependabot/julia/requirement.rb +52 -20
- data/lib/dependabot/julia/update_checker/requirements_updater.rb +41 -19
- 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: 1758439a3175cc549391f423141d544c324c1983ccc26873a271273a1ab5323e
|
|
4
|
+
data.tar.gz: 19a4591beda7d2324c3f0d09404d7b2d3f4d9cdd80f149c153ba87a01b97285b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 41b5c28df20fa883cdc144bb82d0f9c2211da45a73f972c769fb688f3c4c5803eac65d8940f779f9b953c3500b6f0385c24dd6ed83b8c35d81aebdfe497d60e1
|
|
7
|
+
data.tar.gz: a0bfd78e85d9a2146d64c412afe8302929d5a6ab5d722658edb665948819f868ddcf043e9f9a524e1458d95668e636df0ad79d09765f97b2a2f51621301078e7
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
require "dependabot/file_fetchers"
|
|
5
5
|
require "dependabot/file_fetchers/base"
|
|
6
|
+
require "dependabot/julia/registry_client"
|
|
7
|
+
require "dependabot/shared_helpers"
|
|
8
|
+
require "pathname"
|
|
6
9
|
|
|
7
10
|
module Dependabot
|
|
8
11
|
module Julia
|
|
@@ -22,38 +25,50 @@ module Dependabot
|
|
|
22
25
|
# Julia is currently in beta - only fetch files if beta ecosystems are enabled
|
|
23
26
|
return [] unless allow_beta_ecosystems?
|
|
24
27
|
|
|
25
|
-
|
|
28
|
+
# Clone the repository temporarily to let Julia helper identify the correct files
|
|
29
|
+
SharedHelpers.in_a_temporary_repo_directory(directory, repo_contents_path) do |temp_dir|
|
|
30
|
+
fetch_files_using_julia_helper(temp_dir)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
26
33
|
|
|
27
|
-
|
|
28
|
-
fetched_files << project_file
|
|
34
|
+
private
|
|
29
35
|
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
sig { params(temp_dir: T.any(Pathname, String)).returns(T::Array[Dependabot::DependencyFile]) }
|
|
37
|
+
def fetch_files_using_julia_helper(temp_dir)
|
|
38
|
+
# Use Julia helper to identify the correct environment files
|
|
39
|
+
env_files = registry_client.find_environment_files(temp_dir.to_s)
|
|
32
40
|
|
|
33
|
-
|
|
34
|
-
|
|
41
|
+
if env_files.empty? || !env_files["project_file"]
|
|
42
|
+
raise Dependabot::DependencyFileNotFound, "No Project.toml or JuliaProject.toml found."
|
|
43
|
+
end
|
|
35
44
|
|
|
36
|
-
|
|
45
|
+
fetched_files = []
|
|
37
46
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
# Fetch the project file identified by Julia helper
|
|
48
|
+
project_path = T.must(env_files["project_file"])
|
|
49
|
+
project_filename = File.basename(project_path)
|
|
50
|
+
fetched_files << fetch_file_from_host(project_filename)
|
|
51
|
+
|
|
52
|
+
# Fetch the manifest file if Julia helper found one
|
|
53
|
+
manifest_path = env_files["manifest_file"]
|
|
54
|
+
if manifest_path && !manifest_path.empty?
|
|
55
|
+
# Calculate relative path from project to manifest
|
|
56
|
+
project_dir = File.dirname(project_path)
|
|
57
|
+
manifest_relative = Pathname.new(manifest_path).relative_path_from(Pathname.new(project_dir)).to_s
|
|
58
|
+
|
|
59
|
+
# Fetch manifest (handles workspace cases where manifest is in parent directory)
|
|
60
|
+
manifest_file = fetch_file_if_present(manifest_relative)
|
|
61
|
+
fetched_files << manifest_file if manifest_file
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
fetched_files
|
|
49
65
|
end
|
|
50
66
|
|
|
51
|
-
sig { returns(
|
|
52
|
-
def
|
|
53
|
-
@
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
T.nilable(Dependabot::DependencyFile)
|
|
67
|
+
sig { returns(Dependabot::Julia::RegistryClient) }
|
|
68
|
+
def registry_client
|
|
69
|
+
@registry_client ||= T.let(
|
|
70
|
+
Dependabot::Julia::RegistryClient.new(credentials: credentials),
|
|
71
|
+
T.nilable(Dependabot::Julia::RegistryClient)
|
|
57
72
|
)
|
|
58
73
|
end
|
|
59
74
|
end
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
require "toml-rb"
|
|
5
4
|
require "tempfile"
|
|
6
5
|
require "fileutils"
|
|
7
6
|
require "dependabot/dependency"
|
|
@@ -35,8 +34,6 @@ module Dependabot
|
|
|
35
34
|
options: {}
|
|
36
35
|
)
|
|
37
36
|
super
|
|
38
|
-
@parsed_project_file = T.let(nil, T.nilable(T::Hash[String, T.untyped]))
|
|
39
|
-
@parsed_manifest_file = T.let(nil, T.nilable(T::Hash[String, T.untyped]))
|
|
40
37
|
@registry_client = T.let(nil, T.nilable(Dependabot::Julia::RegistryClient))
|
|
41
38
|
@custom_registries = T.let(nil, T.nilable(T::Array[T::Hash[Symbol, T.untyped]]))
|
|
42
39
|
@temp_dir = T.let(nil, T.nilable(String))
|
|
@@ -76,21 +73,11 @@ module Dependabot
|
|
|
76
73
|
sig { returns(String) }
|
|
77
74
|
def write_temp_project_file
|
|
78
75
|
@temp_dir ||= Dir.mktmpdir("julia_project")
|
|
79
|
-
project_path = File.join(@temp_dir,
|
|
76
|
+
project_path = File.join(@temp_dir, T.must(project_file).name)
|
|
80
77
|
File.write(project_path, T.must(project_file).content)
|
|
81
78
|
project_path
|
|
82
79
|
end
|
|
83
80
|
|
|
84
|
-
sig { returns(T.nilable(String)) }
|
|
85
|
-
def write_temp_manifest_file
|
|
86
|
-
return nil unless manifest_file
|
|
87
|
-
|
|
88
|
-
@temp_dir ||= Dir.mktmpdir("julia_project")
|
|
89
|
-
manifest_path = File.join(@temp_dir, "Manifest.toml")
|
|
90
|
-
File.write(manifest_path, T.must(manifest_file).content)
|
|
91
|
-
manifest_path
|
|
92
|
-
end
|
|
93
|
-
|
|
94
81
|
sig { returns(T::Array[Dependabot::Dependency]) }
|
|
95
82
|
def project_file_dependencies
|
|
96
83
|
dependencies = T.let([], T::Array[Dependabot::Dependency])
|
|
@@ -98,22 +85,11 @@ module Dependabot
|
|
|
98
85
|
|
|
99
86
|
# Use DependabotHelper.jl for project parsing
|
|
100
87
|
project_path = write_temp_project_file
|
|
101
|
-
manifest_path = write_temp_manifest_file if manifest_file
|
|
102
88
|
|
|
103
89
|
begin
|
|
104
|
-
result = registry_client.parse_project(
|
|
105
|
-
project_path: project_path,
|
|
106
|
-
manifest_path: manifest_path
|
|
107
|
-
)
|
|
90
|
+
result = registry_client.parse_project(project_path: project_path)
|
|
108
91
|
|
|
109
|
-
if result["error"]
|
|
110
|
-
# Fallback to Ruby TOML parsing if Julia helper fails
|
|
111
|
-
Dependabot.logger.warn(
|
|
112
|
-
"DependabotHelper.jl parsing failed: #{result['error']}, " \
|
|
113
|
-
"falling back to Ruby parsing"
|
|
114
|
-
)
|
|
115
|
-
return fallback_project_file_dependencies
|
|
116
|
-
end
|
|
92
|
+
raise Dependabot::DependencyFileNotParseable, result["error"] if result["error"]
|
|
117
93
|
|
|
118
94
|
# Convert DependabotHelper.jl result to Dependabot::Dependency objects
|
|
119
95
|
dependencies = build_dependencies_from_julia_result(result)
|
|
@@ -128,104 +104,47 @@ module Dependabot
|
|
|
128
104
|
sig { params(result: T::Hash[String, T.untyped]).returns(T::Array[Dependabot::Dependency]) }
|
|
129
105
|
def build_dependencies_from_julia_result(result)
|
|
130
106
|
dependencies = T.let([], T::Array[Dependabot::Dependency])
|
|
131
|
-
parsed_deps = T.cast(result["dependencies"] || [], T::Array[T.untyped])
|
|
132
107
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
requirement_string = T.cast(dep_hash["requirement"] || "*", String)
|
|
138
|
-
resolved_version = T.cast(dep_hash["resolved_version"], T.nilable(String))
|
|
108
|
+
# Process dependencies and weak dependencies (matching CompatHelper.jl behavior)
|
|
109
|
+
# Note: We don't process dev_dependencies/extras to match CompatHelper.jl
|
|
110
|
+
parsed_deps = T.cast(result["dependencies"] || [], T::Array[T.untyped])
|
|
111
|
+
dependencies.concat(build_dependencies_from_dep_list(parsed_deps, ["deps"]))
|
|
139
112
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
dependencies << Dependabot::Dependency.new(
|
|
144
|
-
name: name,
|
|
145
|
-
version: resolved_version,
|
|
146
|
-
requirements: [{
|
|
147
|
-
requirement: requirement_string,
|
|
148
|
-
file: T.must(project_file).name,
|
|
149
|
-
groups: ["runtime"],
|
|
150
|
-
source: nil
|
|
151
|
-
}],
|
|
152
|
-
package_manager: "julia",
|
|
153
|
-
metadata: uuid ? { julia_uuid: uuid } : {}
|
|
154
|
-
)
|
|
155
|
-
end
|
|
113
|
+
parsed_weak_deps = T.cast(result["weak_dependencies"] || [], T::Array[T.untyped])
|
|
114
|
+
dependencies.concat(build_dependencies_from_dep_list(parsed_weak_deps, ["weakdeps"]))
|
|
156
115
|
|
|
157
116
|
dependencies
|
|
158
117
|
end
|
|
159
118
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
119
|
+
sig do
|
|
120
|
+
params(
|
|
121
|
+
dep_list: T::Array[T.untyped],
|
|
122
|
+
groups: T::Array[String]
|
|
123
|
+
).returns(T::Array[Dependabot::Dependency])
|
|
124
|
+
end
|
|
125
|
+
def build_dependencies_from_dep_list(dep_list, groups)
|
|
126
|
+
dep_list.filter_map do |dep_info|
|
|
127
|
+
dep_hash = T.cast(dep_info, T::Hash[String, T.untyped])
|
|
128
|
+
name = T.cast(dep_hash["name"], String)
|
|
170
129
|
next if name == "julia" # Skip Julia version requirement
|
|
171
130
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
# Get the exact version from Manifest.toml if available
|
|
176
|
-
exact_version = version_from_manifest(name)
|
|
131
|
+
uuid = T.cast(dep_hash["uuid"], T.nilable(String))
|
|
132
|
+
# NOTE: Missing "requirement" means no compat entry (any version acceptable)
|
|
133
|
+
requirement_string = T.cast(dep_hash["requirement"], T.nilable(String))
|
|
177
134
|
|
|
178
|
-
|
|
135
|
+
Dependabot::Dependency.new(
|
|
179
136
|
name: name,
|
|
180
|
-
version:
|
|
137
|
+
version: nil, # Julia dependencies don't use locked versions
|
|
181
138
|
requirements: [{
|
|
182
139
|
requirement: requirement_string,
|
|
183
140
|
file: T.must(project_file).name,
|
|
184
|
-
groups:
|
|
141
|
+
groups: groups,
|
|
185
142
|
source: nil
|
|
186
143
|
}],
|
|
187
|
-
package_manager: "julia"
|
|
144
|
+
package_manager: "julia",
|
|
145
|
+
metadata: uuid ? { julia_uuid: uuid } : {}
|
|
188
146
|
)
|
|
189
147
|
end
|
|
190
|
-
|
|
191
|
-
dependencies
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
sig { params(dependency_name: String).returns(T.nilable(String)) }
|
|
195
|
-
def version_from_manifest(dependency_name)
|
|
196
|
-
return nil unless manifest_file
|
|
197
|
-
|
|
198
|
-
# Try using DependabotHelper.jl first
|
|
199
|
-
temp_dir = Dir.mktmpdir("julia_manifest_only")
|
|
200
|
-
manifest_path = File.join(temp_dir, "Manifest.toml")
|
|
201
|
-
File.write(manifest_path, T.must(manifest_file).content)
|
|
202
|
-
|
|
203
|
-
begin
|
|
204
|
-
# We need the UUID for the DependabotHelper.jl call, so fallback to Ruby parsing
|
|
205
|
-
# Note: Future enhancement could add name-only lookup to DependabotHelper.jl
|
|
206
|
-
fallback_version_from_manifest(dependency_name)
|
|
207
|
-
ensure
|
|
208
|
-
FileUtils.rm_rf(temp_dir)
|
|
209
|
-
end
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
sig { params(dependency_name: String).returns(T.nilable(String)) }
|
|
213
|
-
def fallback_version_from_manifest(dependency_name)
|
|
214
|
-
return nil unless manifest_file
|
|
215
|
-
|
|
216
|
-
parsed_manifest = parsed_manifest_file
|
|
217
|
-
|
|
218
|
-
# Look for the dependency in the manifest
|
|
219
|
-
deps_section = T.cast(parsed_manifest["deps"], T.nilable(T::Hash[String, T.untyped]))
|
|
220
|
-
if deps_section && deps_section[dependency_name]
|
|
221
|
-
# Manifest v2 format
|
|
222
|
-
dep_entries = deps_section[dependency_name]
|
|
223
|
-
if dep_entries.is_a?(Array) && dep_entries.first.is_a?(Hash)
|
|
224
|
-
return T.cast(dep_entries.first["version"], T.nilable(String))
|
|
225
|
-
end
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
nil
|
|
229
148
|
end
|
|
230
149
|
|
|
231
150
|
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
|
@@ -236,37 +155,6 @@ module Dependabot
|
|
|
236
155
|
)
|
|
237
156
|
end
|
|
238
157
|
|
|
239
|
-
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
|
240
|
-
def manifest_file
|
|
241
|
-
@manifest_file ||= T.let(
|
|
242
|
-
get_original_file("Manifest.toml") || get_original_file("JuliaManifest.toml"),
|
|
243
|
-
T.nilable(Dependabot::DependencyFile)
|
|
244
|
-
)
|
|
245
|
-
end
|
|
246
|
-
|
|
247
|
-
sig { returns(T::Hash[String, T.untyped]) }
|
|
248
|
-
def parsed_project_file
|
|
249
|
-
return @parsed_project_file if @parsed_project_file
|
|
250
|
-
|
|
251
|
-
parsed_content = T.cast(TomlRB.parse(T.must(project_file).content), T::Hash[String, T.untyped])
|
|
252
|
-
@parsed_project_file = parsed_content
|
|
253
|
-
parsed_content
|
|
254
|
-
rescue TomlRB::ParseError, TomlRB::ValueOverwriteError => e
|
|
255
|
-
raise Dependabot::DependencyFileNotParseable, "Error parsing #{T.must(project_file).name}: #{e.message}"
|
|
256
|
-
end
|
|
257
|
-
|
|
258
|
-
sig { returns(T::Hash[String, T.untyped]) }
|
|
259
|
-
def parsed_manifest_file
|
|
260
|
-
return {} unless manifest_file
|
|
261
|
-
return @parsed_manifest_file if @parsed_manifest_file
|
|
262
|
-
|
|
263
|
-
parsed_content = T.cast(TomlRB.parse(T.must(manifest_file).content), T::Hash[String, T.untyped])
|
|
264
|
-
@parsed_manifest_file = parsed_content
|
|
265
|
-
parsed_content
|
|
266
|
-
rescue TomlRB::ParseError, TomlRB::ValueOverwriteError => e
|
|
267
|
-
raise Dependabot::DependencyFileNotParseable, "Error parsing #{T.must(manifest_file).name}: #{e.message}"
|
|
268
|
-
end
|
|
269
|
-
|
|
270
158
|
sig { override.void }
|
|
271
159
|
def check_required_files
|
|
272
160
|
raise "No Project.toml or JuliaProject.toml!" unless project_file
|
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
|
|
4
4
|
require "toml-rb"
|
|
5
5
|
require "tempfile"
|
|
6
|
+
require "fileutils"
|
|
7
|
+
require "pathname"
|
|
6
8
|
require "dependabot/file_updaters"
|
|
7
9
|
require "dependabot/file_updaters/base"
|
|
8
10
|
require "dependabot/julia/registry_client"
|
|
11
|
+
require "dependabot/notices"
|
|
9
12
|
|
|
10
13
|
module Dependabot
|
|
11
14
|
module Julia
|
|
@@ -17,72 +20,51 @@ module Dependabot
|
|
|
17
20
|
[/(?:Julia)?Project\.toml$/i, /(?:Julia)?Manifest(?:-v[\d.]+)?\.toml$/i]
|
|
18
21
|
end
|
|
19
22
|
|
|
23
|
+
sig { override.returns(T::Array[Dependabot::Notice]) }
|
|
24
|
+
attr_reader :notices
|
|
25
|
+
|
|
26
|
+
sig do
|
|
27
|
+
override.params(
|
|
28
|
+
dependencies: T::Array[Dependabot::Dependency],
|
|
29
|
+
dependency_files: T::Array[Dependabot::DependencyFile],
|
|
30
|
+
credentials: T::Array[Dependabot::Credential],
|
|
31
|
+
repo_contents_path: T.nilable(String),
|
|
32
|
+
options: T::Hash[Symbol, T.untyped]
|
|
33
|
+
).void
|
|
34
|
+
end
|
|
35
|
+
def initialize(dependencies:, dependency_files:, credentials:, repo_contents_path: nil, options: {})
|
|
36
|
+
super
|
|
37
|
+
@notices = T.let([], T::Array[Dependabot::Notice])
|
|
38
|
+
end
|
|
39
|
+
|
|
20
40
|
sig { override.returns(T::Array[Dependabot::DependencyFile]) }
|
|
21
41
|
def updated_dependency_files
|
|
22
|
-
|
|
42
|
+
# If no project file, cannot proceed
|
|
43
|
+
raise "No Project.toml file found" unless project_file
|
|
23
44
|
|
|
24
45
|
# Use DependabotHelper.jl for manifest updating
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
begin
|
|
29
|
-
project_path = write_temp_project_file
|
|
30
|
-
|
|
31
|
-
result = registry_client.update_manifest(
|
|
32
|
-
project_path: project_path,
|
|
33
|
-
updates: build_updates_hash
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
if result["error"]
|
|
37
|
-
# Fallback to Ruby TOML manipulation
|
|
38
|
-
Dependabot.logger.warn(
|
|
39
|
-
"DependabotHelper.jl update failed: #{result['error']}, " \
|
|
40
|
-
"falling back to Ruby updating"
|
|
41
|
-
)
|
|
42
|
-
return fallback_updated_dependency_files
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# Create updated files from DependabotHelper.jl results
|
|
46
|
-
updated_files = build_updated_files_from_result(result)
|
|
47
|
-
rescue StandardError => e
|
|
48
|
-
# Fallback to Ruby TOML manipulation if Julia helper fails
|
|
49
|
-
Dependabot.logger.warn(
|
|
50
|
-
"DependabotHelper.jl update failed with exception: #{e.message}, " \
|
|
51
|
-
"falling back to Ruby updating"
|
|
52
|
-
)
|
|
53
|
-
return fallback_updated_dependency_files
|
|
54
|
-
ensure
|
|
55
|
-
File.delete(project_path) if project_path && File.exist?(project_path)
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
raise "No files changed!" if updated_files.empty?
|
|
60
|
-
|
|
61
|
-
updated_files
|
|
46
|
+
# This works for both standard packages and workspace packages
|
|
47
|
+
updated_files_with_julia_helper
|
|
62
48
|
end
|
|
63
49
|
|
|
64
|
-
# Fallback method using Ruby TOML manipulation
|
|
65
50
|
sig { returns(T::Array[Dependabot::DependencyFile]) }
|
|
66
|
-
def
|
|
51
|
+
def updated_files_with_julia_helper
|
|
67
52
|
updated_files = []
|
|
68
53
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
file: T.must(project_file),
|
|
73
|
-
content: updated_project_content
|
|
74
|
-
)
|
|
75
|
-
end
|
|
54
|
+
SharedHelpers.in_a_temporary_repo_directory(T.must(dependency_files.first).directory, repo_contents_path) do
|
|
55
|
+
updated_project = updated_project_content
|
|
56
|
+
actual_manifest = find_manifest_file
|
|
76
57
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
58
|
+
return project_only_update(updated_project) if actual_manifest.nil?
|
|
59
|
+
|
|
60
|
+
# Work directly in the repo directory - no need for another temp directory
|
|
61
|
+
# This ensures all workspace packages are accessible to Julia's Pkg
|
|
62
|
+
write_temporary_files(updated_project, actual_manifest)
|
|
63
|
+
result = call_julia_helper
|
|
64
|
+
|
|
65
|
+
return handle_julia_helper_error(result, actual_manifest, updated_project) if result["error"]
|
|
66
|
+
|
|
67
|
+
build_updated_files(updated_files, updated_project, actual_manifest, result)
|
|
86
68
|
end
|
|
87
69
|
|
|
88
70
|
raise "No files changed!" if updated_files.empty?
|
|
@@ -90,6 +72,111 @@ module Dependabot
|
|
|
90
72
|
updated_files
|
|
91
73
|
end
|
|
92
74
|
|
|
75
|
+
sig { params(updated_project: String).returns(T::Array[Dependabot::DependencyFile]) }
|
|
76
|
+
def project_only_update(updated_project)
|
|
77
|
+
[updated_file(file: T.must(project_file), content: updated_project)]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
sig do
|
|
81
|
+
params(
|
|
82
|
+
updated_project: String,
|
|
83
|
+
actual_manifest: Dependabot::DependencyFile
|
|
84
|
+
).void
|
|
85
|
+
end
|
|
86
|
+
def write_temporary_files(updated_project, actual_manifest)
|
|
87
|
+
File.write(T.must(project_file).name, updated_project)
|
|
88
|
+
|
|
89
|
+
# Preserve relative paths (e.g., ../Manifest.toml for workspace packages)
|
|
90
|
+
# so Julia's Pkg can find and update the correct shared manifest
|
|
91
|
+
manifest_path = actual_manifest.name
|
|
92
|
+
FileUtils.mkdir_p(File.dirname(manifest_path)) if manifest_path.include?("/")
|
|
93
|
+
File.write(manifest_path, actual_manifest.content)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
sig { returns(T::Hash[String, T.untyped]) }
|
|
97
|
+
def call_julia_helper
|
|
98
|
+
registry_client.update_manifest(
|
|
99
|
+
project_path: Dir.pwd,
|
|
100
|
+
updates: build_updates_hash
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
sig do
|
|
105
|
+
params(
|
|
106
|
+
result: T::Hash[String, T.untyped],
|
|
107
|
+
actual_manifest: Dependabot::DependencyFile,
|
|
108
|
+
updated_project: String
|
|
109
|
+
).returns(T::Array[Dependabot::DependencyFile])
|
|
110
|
+
end
|
|
111
|
+
def handle_julia_helper_error(result, actual_manifest, updated_project)
|
|
112
|
+
error_message = result["error"]
|
|
113
|
+
manifest_path = actual_manifest.name
|
|
114
|
+
|
|
115
|
+
is_resolver_error = resolver_error?(error_message)
|
|
116
|
+
raise error_message unless is_resolver_error
|
|
117
|
+
|
|
118
|
+
add_manifest_update_notice(manifest_path, error_message)
|
|
119
|
+
|
|
120
|
+
# Return only the updated Project.toml
|
|
121
|
+
[updated_file(file: T.must(project_file), content: updated_project)]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
sig { params(error_message: String).returns(T::Boolean) }
|
|
125
|
+
def resolver_error?(error_message)
|
|
126
|
+
error_message.start_with?("Pkg resolver error:") ||
|
|
127
|
+
error_message.include?("Unsatisfiable requirements") ||
|
|
128
|
+
error_message.include?("ResolverError")
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
sig { params(manifest_path: String, error_message: String).void }
|
|
132
|
+
def add_manifest_update_notice(manifest_path, error_message)
|
|
133
|
+
# Resolve relative paths to absolute paths for clarity in user-facing notices
|
|
134
|
+
# Use Pathname.cleanpath to handle any depth of relative paths (e.g., ../../Manifest.toml)
|
|
135
|
+
project_dir = T.must(project_file).directory
|
|
136
|
+
absolute_manifest_path = if manifest_path.start_with?("../", "./")
|
|
137
|
+
# For workspace packages, compute the absolute path
|
|
138
|
+
Pathname.new(File.join(project_dir, manifest_path)).cleanpath.to_s
|
|
139
|
+
else
|
|
140
|
+
# For regular packages, use the manifest path as-is
|
|
141
|
+
File.join(project_dir, manifest_path)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
@notices << Dependabot::Notice.new(
|
|
145
|
+
mode: Dependabot::Notice::NoticeMode::WARN,
|
|
146
|
+
type: "julia_manifest_not_updated",
|
|
147
|
+
package_manager_name: "Pkg",
|
|
148
|
+
title: "Could not update manifest #{absolute_manifest_path}",
|
|
149
|
+
description: "The Julia package manager failed to update the new dependency versions " \
|
|
150
|
+
"in `#{absolute_manifest_path}`:\n\n```\n#{error_message}\n```",
|
|
151
|
+
show_in_pr: true,
|
|
152
|
+
show_alert: true
|
|
153
|
+
)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
sig do
|
|
157
|
+
params(
|
|
158
|
+
updated_files: T::Array[Dependabot::DependencyFile],
|
|
159
|
+
updated_project: String,
|
|
160
|
+
actual_manifest: Dependabot::DependencyFile,
|
|
161
|
+
result: T::Hash[String, T.untyped]
|
|
162
|
+
).void
|
|
163
|
+
end
|
|
164
|
+
def build_updated_files(updated_files, updated_project, actual_manifest, result)
|
|
165
|
+
updated_files << updated_file(file: T.must(project_file), content: updated_project)
|
|
166
|
+
|
|
167
|
+
return unless result["manifest_content"]
|
|
168
|
+
|
|
169
|
+
updated_manifest_content = result["manifest_content"]
|
|
170
|
+
return unless updated_manifest_content != actual_manifest.content
|
|
171
|
+
|
|
172
|
+
manifest_for_update = if result["manifest_path"]
|
|
173
|
+
manifest_file_for_path(result["manifest_path"])
|
|
174
|
+
else
|
|
175
|
+
actual_manifest
|
|
176
|
+
end
|
|
177
|
+
updated_files << updated_file(file: manifest_for_update, content: updated_manifest_content)
|
|
178
|
+
end
|
|
179
|
+
|
|
93
180
|
private
|
|
94
181
|
|
|
95
182
|
sig { returns(T::Hash[String, String]) }
|
|
@@ -98,33 +185,15 @@ module Dependabot
|
|
|
98
185
|
dependencies.each do |dependency|
|
|
99
186
|
next unless dependency.version
|
|
100
187
|
|
|
101
|
-
|
|
188
|
+
uuid = T.cast(dependency.metadata[:julia_uuid], String)
|
|
189
|
+
updates[uuid] = {
|
|
190
|
+
"name" => dependency.name,
|
|
191
|
+
"version" => dependency.version
|
|
192
|
+
}
|
|
102
193
|
end
|
|
103
194
|
updates
|
|
104
195
|
end
|
|
105
196
|
|
|
106
|
-
sig { params(result: T::Hash[String, T.untyped]).returns(T::Array[Dependabot::DependencyFile]) }
|
|
107
|
-
def build_updated_files_from_result(result)
|
|
108
|
-
updated_files = T.let([], T::Array[Dependabot::DependencyFile])
|
|
109
|
-
|
|
110
|
-
if result["project_content"] && result["project_content"] != T.must(project_file).content
|
|
111
|
-
updated_files << updated_file(
|
|
112
|
-
file: T.must(project_file),
|
|
113
|
-
content: result["project_content"]
|
|
114
|
-
)
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
if manifest_file && result["manifest_content"] &&
|
|
118
|
-
result["manifest_content"] != T.must(manifest_file).content
|
|
119
|
-
updated_files << updated_file(
|
|
120
|
-
file: T.must(manifest_file),
|
|
121
|
-
content: result["manifest_content"]
|
|
122
|
-
)
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
updated_files
|
|
126
|
-
end
|
|
127
|
-
|
|
128
197
|
# Helper methods for DependabotHelper.jl integration
|
|
129
198
|
|
|
130
199
|
sig { returns(Dependabot::Julia::RegistryClient) }
|
|
@@ -137,12 +206,54 @@ module Dependabot
|
|
|
137
206
|
)
|
|
138
207
|
end
|
|
139
208
|
|
|
140
|
-
sig { returns(
|
|
141
|
-
def
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
209
|
+
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
|
210
|
+
def find_manifest_file
|
|
211
|
+
# The file fetcher has already identified the correct manifest file
|
|
212
|
+
# For regular packages: manifest in same directory
|
|
213
|
+
# For workspace packages: manifest in parent directory
|
|
214
|
+
# We just need to find it in dependency_files
|
|
215
|
+
project_dir = T.must(project_file).directory
|
|
216
|
+
|
|
217
|
+
dependency_files.find do |f|
|
|
218
|
+
# Use basename to get just the filename, not the full path with ../
|
|
219
|
+
is_manifest = File.basename(f.name).match?(/^(Julia)?Manifest(?:-v[\d.]+)?\.toml$/i)
|
|
220
|
+
is_manifest && (f.directory == project_dir || project_dir.start_with?(f.directory))
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
sig { params(manifest_path: String).returns(Dependabot::DependencyFile) }
|
|
225
|
+
def manifest_file_for_path(manifest_path)
|
|
226
|
+
# manifest_path is relative to the project directory (e.g., "Manifest.toml" or "../Manifest.toml")
|
|
227
|
+
# We need to resolve it to find the actual manifest file in dependency_files
|
|
228
|
+
|
|
229
|
+
# Build the absolute path relative to the project directory
|
|
230
|
+
project_dir = T.must(project_file).directory
|
|
231
|
+
resolved_manifest_path = File.expand_path(manifest_path, project_dir)
|
|
232
|
+
|
|
233
|
+
# Normalize by removing leading "/" to get repo-relative path
|
|
234
|
+
resolved_manifest_path = resolved_manifest_path.sub(%r{^/}, "")
|
|
235
|
+
|
|
236
|
+
# Find the matching manifest file in dependency_files
|
|
237
|
+
found_manifest = dependency_files.find do |f|
|
|
238
|
+
next unless f.name.match?(/^(Julia)?Manifest(?:-v[\d.]+)?\.toml$/i)
|
|
239
|
+
|
|
240
|
+
# Construct the full path for this file and normalize it
|
|
241
|
+
file_path = File.join(f.directory, f.name).sub(%r{^/}, "")
|
|
242
|
+
file_path == resolved_manifest_path
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# If we found the manifest file and the manifest_path matches its name exactly,
|
|
246
|
+
# return the original file to preserve its metadata
|
|
247
|
+
return found_manifest if found_manifest && found_manifest.name == manifest_path
|
|
248
|
+
|
|
249
|
+
# For workspace cases where manifest_path is relative (e.g., "../Manifest.toml"),
|
|
250
|
+
# we need to create a new DependencyFile with the relative path as its name,
|
|
251
|
+
# but copy the content from the found manifest if it exists
|
|
252
|
+
Dependabot::DependencyFile.new(
|
|
253
|
+
name: manifest_path,
|
|
254
|
+
content: found_manifest&.content || "",
|
|
255
|
+
directory: project_dir
|
|
256
|
+
)
|
|
146
257
|
end
|
|
147
258
|
|
|
148
259
|
sig { override.void }
|
|
@@ -164,16 +275,6 @@ module Dependabot
|
|
|
164
275
|
)
|
|
165
276
|
end
|
|
166
277
|
|
|
167
|
-
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
|
168
|
-
def manifest_file
|
|
169
|
-
@manifest_file ||= T.let(
|
|
170
|
-
dependency_files.find do |f|
|
|
171
|
-
f.name.match?(/^(Julia)?Manifest(?:-v[\d.]+)?\.toml$/i)
|
|
172
|
-
end,
|
|
173
|
-
T.nilable(Dependabot::DependencyFile)
|
|
174
|
-
)
|
|
175
|
-
end
|
|
176
|
-
|
|
177
278
|
sig { returns(String) }
|
|
178
279
|
def updated_project_content
|
|
179
280
|
return T.must(T.must(project_file).content) unless project_file
|
|
@@ -196,15 +297,24 @@ module Dependabot
|
|
|
196
297
|
|
|
197
298
|
sig { params(content: String, dependency_name: String, new_requirement: String).returns(String) }
|
|
198
299
|
def update_dependency_requirement_in_content(content, dependency_name, new_requirement)
|
|
199
|
-
#
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
#
|
|
205
|
-
|
|
300
|
+
# Extract the [compat] section to update it specifically
|
|
301
|
+
compat_section_match = content.match(/^\[compat\]\s*\n((?:(?!\[)[^\n]*\n)*?)(?=^\[|\z)/m)
|
|
302
|
+
|
|
303
|
+
if compat_section_match
|
|
304
|
+
compat_section = T.must(compat_section_match[1])
|
|
305
|
+
# Pattern to match the dependency in the compat section
|
|
306
|
+
pattern = /^(\s*#{Regexp.escape(dependency_name)}\s*=\s*)(?:"[^"]*"|'[^']*'|[^\s#\n]+)(\s*(?:\#.*)?)$/
|
|
307
|
+
|
|
308
|
+
if compat_section.match?(pattern)
|
|
309
|
+
# Replace existing entry in compat section
|
|
310
|
+
updated_compat = compat_section.gsub(pattern, "\\1\"#{new_requirement}\"\\2")
|
|
311
|
+
content.sub(T.must(compat_section_match[0]), "[compat]\n#{updated_compat}")
|
|
312
|
+
else
|
|
313
|
+
# Add new entry to existing [compat] section
|
|
314
|
+
add_compat_entry_to_content(content, dependency_name, new_requirement)
|
|
315
|
+
end
|
|
206
316
|
else
|
|
207
|
-
# Add new
|
|
317
|
+
# Add new [compat] section
|
|
208
318
|
add_compat_entry_to_content(content, dependency_name, new_requirement)
|
|
209
319
|
end
|
|
210
320
|
end
|
|
@@ -220,74 +330,6 @@ module Dependabot
|
|
|
220
330
|
content + "\n[compat]\n#{dependency_name} = \"#{requirement}\"\n"
|
|
221
331
|
end
|
|
222
332
|
end
|
|
223
|
-
|
|
224
|
-
sig { returns(String) }
|
|
225
|
-
def build_updated_manifest_content
|
|
226
|
-
return T.must(T.must(manifest_file).content) unless manifest_file
|
|
227
|
-
|
|
228
|
-
content = T.must(T.must(manifest_file).content)
|
|
229
|
-
|
|
230
|
-
dependencies.each do |dependency|
|
|
231
|
-
next unless dependency.version
|
|
232
|
-
|
|
233
|
-
content = update_dependency_version_in_manifest(content, dependency.name, T.must(dependency.version))
|
|
234
|
-
end
|
|
235
|
-
|
|
236
|
-
content
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
sig { params(content: String, dependency_name: String, new_version: String).returns(String) }
|
|
240
|
-
def update_dependency_version_in_manifest(content, dependency_name, new_version)
|
|
241
|
-
# Pattern to find the dependency entry and update its version
|
|
242
|
-
# Matches the [[deps.DependencyName]] section and updates the version line within it
|
|
243
|
-
dep_start = /^\[\[deps\.#{Regexp.escape(dependency_name)}\]\]\s*\n(?:.*\n)*?/
|
|
244
|
-
version_key = /^\s*version\s*=\s*/
|
|
245
|
-
old_version = /(?:"[^"]*"|'[^']*'|[^\s#\n]+)/
|
|
246
|
-
trailing = /\s*(?:\#.*)?$/
|
|
247
|
-
pattern = /(#{dep_start})(#{version_key})#{old_version}(#{trailing})/mx
|
|
248
|
-
|
|
249
|
-
if content.match?(pattern)
|
|
250
|
-
content.gsub(pattern, "\\1\\2\"#{new_version}\"\\3")
|
|
251
|
-
else
|
|
252
|
-
# If pattern doesn't match, fall back to original approach
|
|
253
|
-
Dependabot.logger.warn("Could not find manifest entry for #{dependency_name}, using fallback")
|
|
254
|
-
fallback_update_manifest_content(content, dependency_name, new_version)
|
|
255
|
-
end
|
|
256
|
-
end
|
|
257
|
-
|
|
258
|
-
sig { params(content: String, dependency_name: String, new_version: String).returns(String) }
|
|
259
|
-
def fallback_update_manifest_content(content, dependency_name, new_version)
|
|
260
|
-
# Fallback to parse-and-dump for complex cases
|
|
261
|
-
parsed_manifest = T.cast(TomlRB.parse(content), T::Hash[String, T.untyped])
|
|
262
|
-
|
|
263
|
-
deps_section = T.cast(parsed_manifest["deps"] || {}, T::Hash[String, T.untyped])
|
|
264
|
-
if deps_section[dependency_name]
|
|
265
|
-
dep_entries = deps_section[dependency_name]
|
|
266
|
-
update_dependency_entries(dep_entries, new_version)
|
|
267
|
-
end
|
|
268
|
-
|
|
269
|
-
T.cast(TomlRB.dump(parsed_manifest), String)
|
|
270
|
-
end
|
|
271
|
-
|
|
272
|
-
sig { params(dependency: Dependabot::Dependency, manifest: T::Hash[String, T.untyped]).void }
|
|
273
|
-
def update_dependency_in_manifest(dependency, manifest)
|
|
274
|
-
deps_section = T.cast(manifest["deps"] || {}, T::Hash[String, T.untyped])
|
|
275
|
-
return unless deps_section[dependency.name]
|
|
276
|
-
|
|
277
|
-
dep_entries = deps_section[dependency.name]
|
|
278
|
-
update_dependency_entries(dep_entries, dependency.version)
|
|
279
|
-
end
|
|
280
|
-
|
|
281
|
-
sig { params(dep_entries: T.untyped, version: T.nilable(String)).void }
|
|
282
|
-
def update_dependency_entries(dep_entries, version)
|
|
283
|
-
if dep_entries.is_a?(Array)
|
|
284
|
-
dep_entries.each do |dep_entry|
|
|
285
|
-
dep_entry["version"] = version if dep_entry.is_a?(Hash) && dep_entry["uuid"]
|
|
286
|
-
end
|
|
287
|
-
elsif dep_entries.is_a?(Hash) && dep_entries["uuid"]
|
|
288
|
-
dep_entries["version"] = version
|
|
289
|
-
end
|
|
290
|
-
end
|
|
291
333
|
end
|
|
292
334
|
end
|
|
293
335
|
end
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
# typed: strong
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
require "excon"
|
|
5
4
|
require "dependabot/metadata_finders"
|
|
6
5
|
require "dependabot/metadata_finders/base"
|
|
7
|
-
require "dependabot/registry_client"
|
|
8
6
|
require "dependabot/julia/registry_client"
|
|
9
7
|
require "uri" # Required for URI.parse
|
|
10
|
-
require "toml-rb" # Required for TOML parsing
|
|
11
8
|
|
|
12
9
|
module Dependabot
|
|
13
10
|
module Julia
|
|
@@ -21,9 +18,8 @@ module Dependabot
|
|
|
21
18
|
|
|
22
19
|
sig { override.returns(T.nilable(Dependabot::Source)) }
|
|
23
20
|
def look_up_source
|
|
24
|
-
# Only use authoritative sources from Julia helper
|
|
25
|
-
url_string = source_url_from_julia_helper
|
|
26
|
-
source_url_from_dependency_files
|
|
21
|
+
# Only use authoritative sources from Julia helper
|
|
22
|
+
url_string = source_url_from_julia_helper
|
|
27
23
|
|
|
28
24
|
return nil unless url_string
|
|
29
25
|
|
|
@@ -84,14 +80,6 @@ module Dependabot
|
|
|
84
80
|
Dependabot.logger.error("Invalid URI for dependency #{dependency.name}: #{url_string} - #{e.message}")
|
|
85
81
|
nil
|
|
86
82
|
end
|
|
87
|
-
|
|
88
|
-
sig { returns(T.nilable(String)) }
|
|
89
|
-
def source_url_from_dependency_files
|
|
90
|
-
# MetadataFinder doesn't have access to dependency_files
|
|
91
|
-
# This would typically be handled by FileParser or other components
|
|
92
|
-
# For now, we'll skip this strategy and rely on the Julia helper
|
|
93
|
-
nil
|
|
94
|
-
end
|
|
95
83
|
end
|
|
96
84
|
end
|
|
97
85
|
end
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# typed:
|
|
1
|
+
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require "time"
|
|
@@ -72,31 +72,57 @@ module Dependabot
|
|
|
72
72
|
).returns(T::Array[Dependabot::Package::PackageRelease])
|
|
73
73
|
end
|
|
74
74
|
def build_releases_for_versions(registry_client, available_versions, uuid)
|
|
75
|
-
|
|
75
|
+
# Use batch operation to fetch all release dates at once
|
|
76
|
+
release_dates = fetch_release_dates_batch(registry_client, available_versions, uuid)
|
|
76
77
|
|
|
77
|
-
available_versions.
|
|
78
|
+
available_versions.map do |version_string|
|
|
78
79
|
version = Julia::Version.new(version_string)
|
|
79
|
-
release_date =
|
|
80
|
+
release_date = release_dates[version_string]
|
|
80
81
|
|
|
81
|
-
|
|
82
|
+
create_package_release(version, release_date)
|
|
82
83
|
end
|
|
83
|
-
|
|
84
|
-
releases
|
|
85
84
|
end
|
|
86
85
|
|
|
87
86
|
sig do
|
|
88
87
|
params(
|
|
89
88
|
registry_client: RegistryClient,
|
|
90
|
-
|
|
89
|
+
available_versions: T::Array[String],
|
|
91
90
|
uuid: T.nilable(String)
|
|
92
|
-
).returns(T.nilable(Time))
|
|
91
|
+
).returns(T::Hash[String, T.nilable(Time)])
|
|
93
92
|
end
|
|
94
|
-
def
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
93
|
+
def fetch_release_dates_batch(registry_client, available_versions, uuid)
|
|
94
|
+
return {} if available_versions.empty?
|
|
95
|
+
|
|
96
|
+
packages_versions = [{
|
|
97
|
+
name: dependency.name,
|
|
98
|
+
uuid: uuid || "",
|
|
99
|
+
versions: available_versions
|
|
100
|
+
}]
|
|
101
|
+
|
|
102
|
+
result = registry_client.batch_fetch_version_release_dates(packages_versions)
|
|
103
|
+
dates_for_package = result[dependency.name] || {}
|
|
104
|
+
|
|
105
|
+
convert_dates_to_time_objects(dates_for_package)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
sig do
|
|
109
|
+
params(
|
|
110
|
+
dates_hash: T::Hash[String, T.untyped]
|
|
111
|
+
).returns(T::Hash[String, T.nilable(Time)])
|
|
112
|
+
end
|
|
113
|
+
def convert_dates_to_time_objects(dates_hash)
|
|
114
|
+
dates_hash.transform_values do |date_value|
|
|
115
|
+
convert_single_date(date_value)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
sig { params(date_value: T.untyped).returns(T.nilable(Time)) }
|
|
120
|
+
def convert_single_date(date_value)
|
|
121
|
+
return nil if date_value.nil?
|
|
122
|
+
return nil if date_value.is_a?(Hash) && date_value["error"]
|
|
123
|
+
|
|
124
|
+
Time.parse(date_value.to_s)
|
|
125
|
+
rescue ArgumentError, TypeError
|
|
100
126
|
nil
|
|
101
127
|
end
|
|
102
128
|
|
|
@@ -14,10 +14,10 @@ module Dependabot
|
|
|
14
14
|
PACKAGE_MANAGER_COMMAND = T.let("julia", String)
|
|
15
15
|
# Julia versions as of June 2025:
|
|
16
16
|
# - 1.10 is the LTS (Long Term Support) version
|
|
17
|
-
# - 1.
|
|
17
|
+
# - 1.12 is the current stable version
|
|
18
18
|
# Update these constants when new LTS or major versions are released
|
|
19
19
|
MINIMUM_VERSION = T.let("1.10", String) # LTS version
|
|
20
|
-
CURRENT_VERSION = T.let("1.
|
|
20
|
+
CURRENT_VERSION = T.let("1.12", String) # Current stable version
|
|
21
21
|
|
|
22
22
|
sig { returns(T.nilable(String)) }
|
|
23
23
|
def self.detected_version
|
|
@@ -87,24 +87,6 @@ module Dependabot
|
|
|
87
87
|
nil
|
|
88
88
|
end
|
|
89
89
|
|
|
90
|
-
sig do
|
|
91
|
-
params(
|
|
92
|
-
project_path: String,
|
|
93
|
-
package_name: String,
|
|
94
|
-
target_version: String
|
|
95
|
-
).returns(T::Hash[String, T.untyped])
|
|
96
|
-
end
|
|
97
|
-
def check_update_compatibility(project_path, package_name, target_version)
|
|
98
|
-
call_julia_helper(
|
|
99
|
-
function: "check_update_compatibility",
|
|
100
|
-
args: {
|
|
101
|
-
project_path: project_path,
|
|
102
|
-
package_name: package_name,
|
|
103
|
-
target_version: target_version
|
|
104
|
-
}
|
|
105
|
-
)
|
|
106
|
-
end
|
|
107
|
-
|
|
108
90
|
sig do
|
|
109
91
|
params(
|
|
110
92
|
project_path: String,
|
|
@@ -129,6 +111,24 @@ module Dependabot
|
|
|
129
111
|
)
|
|
130
112
|
end
|
|
131
113
|
|
|
114
|
+
sig { params(directory: String).returns(T::Hash[String, String]) }
|
|
115
|
+
def find_environment_files(directory)
|
|
116
|
+
result = call_julia_helper(
|
|
117
|
+
function: "find_environment_files",
|
|
118
|
+
args: { directory: directory }
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
return {} if result["error"]
|
|
122
|
+
|
|
123
|
+
{
|
|
124
|
+
"project_file" => result["project_file"],
|
|
125
|
+
"manifest_file" => result["manifest_file"]
|
|
126
|
+
}
|
|
127
|
+
rescue StandardError => e
|
|
128
|
+
Dependabot.logger.warn("Failed to find environment files in #{directory}: #{e.message}")
|
|
129
|
+
{}
|
|
130
|
+
end
|
|
131
|
+
|
|
132
132
|
sig do
|
|
133
133
|
params(
|
|
134
134
|
manifest_path: String,
|
|
@@ -160,22 +160,6 @@ module Dependabot
|
|
|
160
160
|
)
|
|
161
161
|
end
|
|
162
162
|
|
|
163
|
-
sig do
|
|
164
|
-
params(
|
|
165
|
-
package_name: String,
|
|
166
|
-
source_url: String
|
|
167
|
-
).returns(T::Hash[String, T.untyped])
|
|
168
|
-
end
|
|
169
|
-
def extract_package_metadata_from_url(package_name, source_url)
|
|
170
|
-
call_julia_helper(
|
|
171
|
-
function: "extract_package_metadata_from_url",
|
|
172
|
-
args: {
|
|
173
|
-
package_name: package_name,
|
|
174
|
-
source_url: source_url
|
|
175
|
-
}
|
|
176
|
-
)
|
|
177
|
-
end
|
|
178
|
-
|
|
179
163
|
sig do
|
|
180
164
|
params(
|
|
181
165
|
project_path: String,
|
|
@@ -269,6 +253,68 @@ module Dependabot
|
|
|
269
253
|
[]
|
|
270
254
|
end
|
|
271
255
|
|
|
256
|
+
# ============================================================================
|
|
257
|
+
# BATCH OPERATIONS
|
|
258
|
+
# ============================================================================
|
|
259
|
+
|
|
260
|
+
sig { params(dependencies: T::Array[Dependabot::Dependency]).returns(T::Hash[String, T.untyped]) }
|
|
261
|
+
def batch_fetch_package_info(dependencies)
|
|
262
|
+
packages = dependencies.map do |dep|
|
|
263
|
+
{
|
|
264
|
+
name: dep.name,
|
|
265
|
+
uuid: dep.metadata[:julia_uuid] || ""
|
|
266
|
+
}
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
call_julia_helper(
|
|
270
|
+
function: "batch_get_package_info",
|
|
271
|
+
args: { packages: packages }
|
|
272
|
+
)
|
|
273
|
+
rescue StandardError => e
|
|
274
|
+
Dependabot.logger.error("Failed to batch fetch package info: #{e.message}")
|
|
275
|
+
{}
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
sig do
|
|
279
|
+
params(
|
|
280
|
+
packages_versions: T::Array[T::Hash[Symbol, T.untyped]]
|
|
281
|
+
).returns(T::Hash[String, T::Hash[String, T.nilable(String)]])
|
|
282
|
+
end
|
|
283
|
+
def batch_fetch_version_release_dates(packages_versions)
|
|
284
|
+
result = call_julia_helper(
|
|
285
|
+
function: "batch_get_version_release_dates",
|
|
286
|
+
args: { packages_versions: packages_versions }
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
# Convert the result to a more Ruby-friendly format
|
|
290
|
+
result.transform_values do |dates|
|
|
291
|
+
next dates if dates.is_a?(Hash) && dates["error"]
|
|
292
|
+
|
|
293
|
+
dates.is_a?(Hash) ? dates : {}
|
|
294
|
+
end
|
|
295
|
+
rescue StandardError => e
|
|
296
|
+
Dependabot.logger.error("Failed to batch fetch version release dates: #{e.message}")
|
|
297
|
+
{}
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
sig { params(dependencies: T::Array[Dependabot::Dependency]).returns(T::Hash[String, T.untyped]) }
|
|
301
|
+
def batch_fetch_available_versions(dependencies)
|
|
302
|
+
packages = dependencies.map do |dep|
|
|
303
|
+
{
|
|
304
|
+
name: dep.name,
|
|
305
|
+
uuid: dep.metadata[:julia_uuid] || ""
|
|
306
|
+
}
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
call_julia_helper(
|
|
310
|
+
function: "batch_get_available_versions",
|
|
311
|
+
args: { packages: packages }
|
|
312
|
+
)
|
|
313
|
+
rescue StandardError => e
|
|
314
|
+
Dependabot.logger.error("Failed to batch fetch available versions: #{e.message}")
|
|
315
|
+
{}
|
|
316
|
+
end
|
|
317
|
+
|
|
272
318
|
private
|
|
273
319
|
|
|
274
320
|
sig { returns(T::Array[T::Hash[Symbol, String]]) }
|
|
@@ -14,16 +14,17 @@ module Dependabot
|
|
|
14
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
|
-
#
|
|
18
|
-
return [new(">= 0")] if requirement_string.nil? || requirement_string.empty?
|
|
17
|
+
# Note: Missing compat entry (nil/empty) means any version is acceptable
|
|
18
|
+
return [new(">= 0")] if requirement_string.nil? || requirement_string.empty?
|
|
19
19
|
|
|
20
20
|
# Split by comma for multiple constraints
|
|
21
21
|
constraints = requirement_string.split(",").map(&:strip)
|
|
22
22
|
|
|
23
23
|
constraints.map do |constraint|
|
|
24
|
-
# Handle Julia-specific patterns
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
# Handle Julia-specific patterns - returns an array of gem requirement strings
|
|
25
|
+
normalized_constraints = normalize_julia_constraint(constraint)
|
|
26
|
+
# Pass the array to Gem::Requirement, which accepts multiple conditions
|
|
27
|
+
new(normalized_constraints)
|
|
27
28
|
end
|
|
28
29
|
rescue Gem::Requirement::BadRequirementError
|
|
29
30
|
[new(">= 0")]
|
|
@@ -41,38 +42,69 @@ module Dependabot
|
|
|
41
42
|
version
|
|
42
43
|
end
|
|
43
44
|
|
|
44
|
-
sig { params(constraint: String).returns(String) }
|
|
45
|
+
sig { params(constraint: String).returns(T::Array[String]) }
|
|
45
46
|
def self.normalize_julia_constraint(constraint)
|
|
46
47
|
return normalize_caret_constraint(constraint) if constraint.match?(/^\^(\d+(?:\.\d+)*)/)
|
|
47
48
|
return normalize_tilde_constraint(constraint) if constraint.match?(/^~(\d+(?:\.\d+)*)/)
|
|
48
49
|
return normalize_range_constraint(constraint) if constraint.match?(/^(\d+(?:\.\d+)*)-(\d+(?:\.\d+)*)$/)
|
|
49
50
|
|
|
51
|
+
# Julia treats plain version numbers as caret constraints (implicit ^)
|
|
52
|
+
# e.g., "1.2.3" is equivalent to "^1.2.3" which means ">= 1.2.3, < 2.0.0"
|
|
53
|
+
# See: https://pkgdocs.julialang.org/v1/compatibility/
|
|
54
|
+
return normalize_caret_constraint("^#{constraint}") if constraint.match?(/^(\d+(?:\.\d+)*)$/)
|
|
55
|
+
|
|
50
56
|
# Return as-is for standard gem requirements (>=, <=, ==, etc.)
|
|
51
|
-
constraint
|
|
57
|
+
[constraint]
|
|
52
58
|
end
|
|
53
59
|
|
|
54
|
-
sig { params(constraint: String).returns(String) }
|
|
60
|
+
sig { params(constraint: String).returns(T::Array[String]) }
|
|
55
61
|
private_class_method def self.normalize_caret_constraint(constraint)
|
|
56
62
|
version = T.must(constraint[1..-1])
|
|
57
63
|
parts = version.split(".")
|
|
58
|
-
major = T.must(parts[0])
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
64
|
+
major = T.must(parts[0]).to_i
|
|
65
|
+
minor = parts[1].to_i
|
|
66
|
+
patch = parts[2].to_i
|
|
67
|
+
|
|
68
|
+
# Julia caret semantics:
|
|
69
|
+
# - For 0.0.x: compatible within patch (e.g., 0.0.5 -> 0.0.x, < 0.0.6 or < 0.1.0?)
|
|
70
|
+
# - For 0.x.y: compatible within minor (e.g., 0.34.6 -> 0.34.x, < 0.35.0)
|
|
71
|
+
# - For x.y.z (x > 0): compatible within major (e.g., 1.2.3 -> 1.x.x, < 2.0.0)
|
|
72
|
+
if major.zero? && minor.zero?
|
|
73
|
+
# 0.0.x versions: bump patch
|
|
74
|
+
[">= #{version}", "< 0.0.#{patch + 1}"]
|
|
75
|
+
elsif major.zero?
|
|
76
|
+
# 0.x.y versions: bump minor (0.34.6 -> < 0.35.0)
|
|
77
|
+
[">= #{version}", "< 0.#{minor + 1}.0"]
|
|
78
|
+
else
|
|
79
|
+
# x.y.z versions where x > 0: bump major
|
|
80
|
+
[">= #{version}", "< #{major + 1}.0.0"]
|
|
81
|
+
end
|
|
62
82
|
end
|
|
63
83
|
|
|
64
|
-
sig { params(constraint: String).returns(String) }
|
|
84
|
+
sig { params(constraint: String).returns(T::Array[String]) }
|
|
65
85
|
private_class_method def self.normalize_tilde_constraint(constraint)
|
|
66
86
|
version = T.must(constraint[1..-1])
|
|
67
87
|
parts = version.split(".")
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
88
|
+
major = T.must(parts[0]).to_i
|
|
89
|
+
minor = parts[1].to_i
|
|
90
|
+
|
|
91
|
+
# Julia tilde semantics (similar to npm):
|
|
92
|
+
# - For 0.0.x: compatible within patch (same as caret)
|
|
93
|
+
# - For 0.x.y or x.y.z: compatible within minor (bump minor)
|
|
94
|
+
if major.zero? && minor.zero?
|
|
95
|
+
# 0.0.x versions: bump patch
|
|
96
|
+
patch = parts[2].to_i
|
|
97
|
+
[">= #{version}", "< 0.0.#{patch + 1}"]
|
|
98
|
+
elsif major.zero?
|
|
99
|
+
# 0.x.y versions: bump minor (same as caret for 0.x)
|
|
100
|
+
[">= #{version}", "< 0.#{minor + 1}.0"]
|
|
101
|
+
else
|
|
102
|
+
# x.y.z versions where x > 0: bump minor only
|
|
103
|
+
[">= #{version}", "< #{major}.#{minor + 1}.0"]
|
|
104
|
+
end
|
|
73
105
|
end
|
|
74
106
|
|
|
75
|
-
sig { params(constraint: String).returns(String) }
|
|
107
|
+
sig { params(constraint: String).returns(T::Array[String]) }
|
|
76
108
|
private_class_method def self.normalize_range_constraint(constraint)
|
|
77
109
|
start_version, end_version = constraint.split("-")
|
|
78
110
|
end_parts = T.must(end_version).split(".")
|
|
@@ -85,7 +117,7 @@ module Dependabot
|
|
|
85
117
|
"#{T.must(end_parts[0]).to_i + 1}.0.0"
|
|
86
118
|
end
|
|
87
119
|
|
|
88
|
-
">= #{start_version}, < #{next_minor}"
|
|
120
|
+
[">= #{start_version}", "< #{next_minor}"]
|
|
89
121
|
end
|
|
90
122
|
end
|
|
91
123
|
end
|
|
@@ -53,8 +53,8 @@ module Dependabot
|
|
|
53
53
|
def update_requirement(requirement, target_version)
|
|
54
54
|
current_requirement = requirement[:requirement]
|
|
55
55
|
|
|
56
|
-
# If requirement is
|
|
57
|
-
new_requirement = if current_requirement.nil?
|
|
56
|
+
# If requirement is nil (no compat entry), use target version
|
|
57
|
+
new_requirement = if current_requirement.nil?
|
|
58
58
|
target_version.to_s
|
|
59
59
|
else
|
|
60
60
|
updated_version_requirement(current_requirement, target_version)
|
|
@@ -65,24 +65,46 @@ module Dependabot
|
|
|
65
65
|
|
|
66
66
|
sig { params(requirement_string: String, target_version: Dependabot::Julia::Version).returns(String) }
|
|
67
67
|
def updated_version_requirement(requirement_string, target_version)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
68
|
+
# Don't update range requirements (e.g., "0.34-0.35") - these are explicit manual constraints
|
|
69
|
+
return requirement_string if requirement_string.match?(/^\d+(?:\.\d+)*-\d+(?:\.\d+)*$/)
|
|
70
|
+
|
|
71
|
+
# Parse all constraints in the requirement string
|
|
72
|
+
reqs = Dependabot::Julia::Requirement.requirements_array(requirement_string)
|
|
73
|
+
|
|
74
|
+
# Check if any requirement is satisfied by the target version
|
|
75
|
+
# Note: This uses the implicit caret semantics from the Requirement class
|
|
76
|
+
return requirement_string if reqs.any? { |req| req.satisfied_by?(target_version) }
|
|
77
|
+
|
|
78
|
+
# Otherwise, append a new requirement that includes the target version
|
|
79
|
+
# Following CompatHelper.jl's approach: use major.minor for versions >= 1.0,
|
|
80
|
+
# 0.minor for 0.x versions, and 0.0.patch for 0.0.x versions
|
|
81
|
+
new_spec = simplified_version_spec(target_version)
|
|
82
|
+
|
|
83
|
+
# Append the new spec to the existing requirement (CompatHelper KeepEntry behavior)
|
|
84
|
+
# Detect whether the existing requirement uses spaces after commas and preserve that format
|
|
85
|
+
# and default to ", " if no commas found
|
|
86
|
+
separator = requirement_string.include?(",") && !requirement_string.include?(", ") ? "," : ", "
|
|
87
|
+
"#{requirement_string}#{separator}#{new_spec}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
sig { params(target_version: Dependabot::Julia::Version).returns(String) }
|
|
91
|
+
def simplified_version_spec(target_version)
|
|
92
|
+
# Follow CompatHelper.jl's compat_version_number logic:
|
|
93
|
+
# - major > 0: use "major.minor"
|
|
94
|
+
# - major == 0, minor > 0: use "0.minor"
|
|
95
|
+
# - major == 0, minor == 0: use "0.0.patch"
|
|
96
|
+
# Note: CompatHelper always returns plain versions (no ^ or ~ prefix)
|
|
97
|
+
# Coerce segments to integers (segments may be Integer or String or nil)
|
|
98
|
+
major = (target_version.segments[0] || 0).to_i
|
|
99
|
+
minor = (target_version.segments[1] || 0).to_i
|
|
100
|
+
patch = (target_version.segments[2] || 0).to_i
|
|
101
|
+
|
|
102
|
+
if major.positive?
|
|
103
|
+
"#{major}.#{minor}"
|
|
104
|
+
elsif minor.positive?
|
|
105
|
+
"0.#{minor}"
|
|
83
106
|
else
|
|
84
|
-
|
|
85
|
-
target_version.to_s
|
|
107
|
+
"0.0.#{patch}"
|
|
86
108
|
end
|
|
87
109
|
end
|
|
88
110
|
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.346.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.346.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.346.0
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: debug
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -260,7 +260,7 @@ licenses:
|
|
|
260
260
|
- MIT
|
|
261
261
|
metadata:
|
|
262
262
|
bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
|
|
263
|
-
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.
|
|
263
|
+
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.346.0
|
|
264
264
|
rdoc_options: []
|
|
265
265
|
require_paths:
|
|
266
266
|
- lib
|