dependabot-julia 0.342.2

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.
@@ -0,0 +1,99 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "excon"
5
+ require "dependabot/metadata_finders"
6
+ require "dependabot/metadata_finders/base"
7
+ require "dependabot/registry_client"
8
+ require "dependabot/julia/registry_client"
9
+ require "uri" # Required for URI.parse
10
+ require "toml-rb" # Required for TOML parsing
11
+
12
+ module Dependabot
13
+ module Julia
14
+ class MetadataFinder < Dependabot::MetadataFinders::Base
15
+ extend T::Sig
16
+
17
+ # The public source_url method is inherited from Base.
18
+ # We need to implement look_up_source as a private method.
19
+
20
+ private
21
+
22
+ sig { override.returns(T.nilable(Dependabot::Source)) }
23
+ def look_up_source
24
+ # Only use authoritative sources from Julia helper or dependency files
25
+ url_string = source_url_from_julia_helper ||
26
+ source_url_from_dependency_files
27
+
28
+ return nil unless url_string
29
+
30
+ parse_source_url(url_string)
31
+ end
32
+
33
+ sig { returns(T.nilable(String)) }
34
+ def source_url_from_julia_helper
35
+ uuid = T.cast(dependency.metadata[:julia_uuid], T.nilable(String))
36
+ result = registry_client.find_package_source_url(dependency.name, uuid)
37
+ error = T.cast(result["error"], T.nilable(T.any(String, T::Boolean)))
38
+ return nil if error
39
+
40
+ T.cast(result["source_url"], T.nilable(String))
41
+ rescue StandardError => e
42
+ Dependabot.logger.warn("Failed to get source URL from Julia helper: #{e.message}")
43
+ nil
44
+ end
45
+
46
+ sig { returns(Dependabot::Julia::RegistryClient) }
47
+ def registry_client
48
+ @registry_client ||= T.let(
49
+ Dependabot::Julia::RegistryClient.new(
50
+ credentials: credentials
51
+ ),
52
+ T.nilable(Dependabot::Julia::RegistryClient)
53
+ )
54
+ end
55
+
56
+ sig { params(url_string: String).returns(T.nilable(Dependabot::Source)) }
57
+ def parse_source_url(url_string)
58
+ uri = URI.parse(url_string)
59
+ hostname = uri.host
60
+ return nil unless hostname
61
+
62
+ # Extract repository path and clean it
63
+ path = T.must(uri.path).delete_prefix("/").delete_suffix(".git")
64
+ path_parts = path.split("/")
65
+ return nil if path_parts.length < 2
66
+
67
+ repo = "#{path_parts[0]}/#{path_parts[1]}"
68
+
69
+ # Determine the provider based on hostname
70
+ provider = case hostname
71
+ when "github.com" then "github"
72
+ when "gitlab.com" then "gitlab"
73
+ when /\A.*\.gitlab\.io\z/ then "gitlab"
74
+ else
75
+ Dependabot.logger.info("Unknown SCM provider for #{hostname}, using generic")
76
+ return nil # Return nil for unknown providers
77
+ end
78
+
79
+ Dependabot::Source.new(
80
+ provider: provider,
81
+ repo: repo
82
+ )
83
+ rescue URI::InvalidURIError => e
84
+ Dependabot.logger.error("Invalid URI for dependency #{dependency.name}: #{url_string} - #{e.message}")
85
+ nil
86
+ 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
+ end
96
+ end
97
+ end
98
+
99
+ Dependabot::MetadataFinders.register("julia", Dependabot::Julia::MetadataFinder)
@@ -0,0 +1,129 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "time"
5
+ require "dependabot/julia/registry_client"
6
+ require "dependabot/julia/version"
7
+ require "dependabot/package/package_release"
8
+ require "dependabot/package/package_language"
9
+
10
+ module Dependabot
11
+ module Julia
12
+ module Package
13
+ class PackageDetailsFetcher
14
+ extend T::Sig
15
+
16
+ PACKAGE_LANGUAGE = "julia"
17
+
18
+ sig do
19
+ params(
20
+ dependency: Dependabot::Dependency,
21
+ credentials: T::Array[Dependabot::Credential],
22
+ custom_registries: T::Array[T::Hash[Symbol, String]]
23
+ ).void
24
+ end
25
+ def initialize(dependency:, credentials:, custom_registries: [])
26
+ @dependency = dependency
27
+ @credentials = credentials
28
+ @custom_registries = custom_registries
29
+ end
30
+
31
+ sig { returns(Dependabot::Dependency) }
32
+ attr_reader :dependency
33
+
34
+ sig { returns(T::Array[Dependabot::Credential]) }
35
+ attr_reader :credentials
36
+
37
+ sig { returns(T::Array[T::Hash[Symbol, String]]) }
38
+ attr_reader :custom_registries
39
+
40
+ sig { returns(T::Array[Dependabot::Package::PackageRelease]) }
41
+ def fetch_package_releases
42
+ releases = T.let([], T::Array[Dependabot::Package::PackageRelease])
43
+
44
+ begin
45
+ registry_client = RegistryClient.new(
46
+ credentials: credentials,
47
+ custom_registries: custom_registries
48
+ )
49
+ uuid = T.cast(dependency.metadata[:julia_uuid], T.nilable(String))
50
+
51
+ # Fetch all available versions
52
+ available_versions = registry_client.fetch_available_versions(dependency.name, uuid)
53
+ return releases if available_versions.empty?
54
+
55
+ releases = build_releases_for_versions(registry_client, available_versions, uuid)
56
+ mark_latest_release(releases)
57
+
58
+ releases
59
+ rescue StandardError => e
60
+ Dependabot.logger.error("Error while fetching package releases for #{dependency.name}: #{e.message}")
61
+ releases
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ sig do
68
+ params(
69
+ registry_client: RegistryClient,
70
+ available_versions: T::Array[String],
71
+ uuid: T.nilable(String)
72
+ ).returns(T::Array[Dependabot::Package::PackageRelease])
73
+ end
74
+ def build_releases_for_versions(registry_client, available_versions, uuid)
75
+ releases = T.let([], T::Array[Dependabot::Package::PackageRelease])
76
+
77
+ available_versions.each do |version_string|
78
+ version = Julia::Version.new(version_string)
79
+ release_date = fetch_release_date_safely(registry_client, version_string, uuid)
80
+
81
+ releases << create_package_release(version, release_date)
82
+ end
83
+
84
+ releases
85
+ end
86
+
87
+ sig do
88
+ params(
89
+ registry_client: RegistryClient,
90
+ version_string: String,
91
+ uuid: T.nilable(String)
92
+ ).returns(T.nilable(Time))
93
+ end
94
+ def fetch_release_date_safely(registry_client, version_string, uuid)
95
+ registry_client.fetch_version_release_date(dependency.name, version_string, uuid)
96
+ rescue StandardError => e
97
+ Dependabot.logger.warn(
98
+ "Failed to fetch release info for #{dependency.name} version #{version_string}: #{e.message}"
99
+ )
100
+ nil
101
+ end
102
+
103
+ sig do
104
+ params(
105
+ version: Julia::Version,
106
+ release_date: T.nilable(Time)
107
+ ).returns(Dependabot::Package::PackageRelease)
108
+ end
109
+ def create_package_release(version, release_date)
110
+ Dependabot::Package::PackageRelease.new(
111
+ version: version,
112
+ released_at: release_date,
113
+ latest: false, # Will be determined later
114
+ yanked: false, # Julia registries don't support yanked packages
115
+ language: Dependabot::Package::PackageLanguage.new(name: PACKAGE_LANGUAGE)
116
+ )
117
+ end
118
+
119
+ sig { params(releases: T::Array[Dependabot::Package::PackageRelease]).void }
120
+ def mark_latest_release(releases)
121
+ return if releases.empty?
122
+
123
+ latest_release = releases.max_by(&:version)
124
+ latest_release&.instance_variable_set(:@latest, true)
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,71 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+ require "dependabot/ecosystem"
6
+ require "dependabot/shared_helpers"
7
+ require "dependabot/version"
8
+
9
+ module Dependabot
10
+ module Julia
11
+ class PackageManager < Ecosystem::VersionManager
12
+ extend T::Sig
13
+
14
+ PACKAGE_MANAGER_COMMAND = T.let("julia", String)
15
+ # Julia versions as of June 2025:
16
+ # - 1.10 is the LTS (Long Term Support) version
17
+ # - 1.11 is the current stable version
18
+ # Update these constants when new LTS or major versions are released
19
+ MINIMUM_VERSION = T.let("1.10", String) # LTS version
20
+ CURRENT_VERSION = T.let("1.11", String) # Current stable version
21
+
22
+ sig { returns(T.nilable(String)) }
23
+ def self.detected_version
24
+ # Try to detect Julia version by executing `julia --version`
25
+ output = T.let(
26
+ Dependabot::SharedHelpers.run_shell_command("julia --version"),
27
+ String
28
+ )
29
+
30
+ # Parse output like "julia version 1.10.0" or "julia version 1.6.7"
31
+ version_match = output.match(/julia version (\d+\.\d+(?:\.\d+)?)/)
32
+ return version_match[1] if version_match
33
+
34
+ # If we can't parse the version, log and fallback
35
+ Dependabot.logger.warn("Could not parse Julia version from output: #{output}")
36
+ MINIMUM_VERSION
37
+ rescue Dependabot::SharedHelpers::HelperSubprocessFailed => e
38
+ Dependabot.logger.info("Julia not found or failed to execute: #{e.message}")
39
+ MINIMUM_VERSION
40
+ rescue StandardError => e
41
+ Dependabot.logger.error("Error detecting Julia version: #{e.message}")
42
+ MINIMUM_VERSION
43
+ end
44
+
45
+ sig { void }
46
+ def initialize
47
+ detected_ver_str = self.class.detected_version
48
+ # detected_version always returns a string (either detected or MINIMUM_VERSION fallback),
49
+ # so we can safely use T.must here
50
+ super(
51
+ name: "julia",
52
+ detected_version: Dependabot::Version.new(T.must(detected_ver_str)),
53
+ version: Dependabot::Version.new(T.must(detected_ver_str)),
54
+ supported_versions: [
55
+ Dependabot::Version.new(MINIMUM_VERSION),
56
+ Dependabot::Version.new(CURRENT_VERSION)
57
+ ],
58
+ deprecated_versions: []
59
+ )
60
+ end
61
+
62
+ sig { returns(T::Hash[T.untyped, T.untyped]) }
63
+ def ecosystem
64
+ {
65
+ package_manager: "julia",
66
+ version: version # This will use the potentially configured version
67
+ }
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,340 @@
1
+ # Julia Registry Client with DependabotHelper.jl integration
2
+ # typed: strict
3
+ # frozen_string_literal: true
4
+
5
+ require "time"
6
+ require "dependabot/credential"
7
+ require "dependabot/julia/version"
8
+ require "dependabot/shared_helpers"
9
+ require "dependabot/errors"
10
+
11
+ module Dependabot
12
+ module Julia
13
+ class RegistryClient
14
+ extend T::Sig
15
+
16
+ sig do
17
+ params(credentials: T::Array[Dependabot::Credential], custom_registries: T::Array[T::Hash[Symbol, String]]).void
18
+ end
19
+ def initialize(credentials:, custom_registries: [])
20
+ @credentials = credentials
21
+ @custom_registries = custom_registries
22
+ end
23
+
24
+ sig { params(package_name: String, package_uuid: T.nilable(String)).returns(T.nilable(Gem::Version)) }
25
+ def fetch_latest_version(package_name, package_uuid = nil)
26
+ # Use custom registries if available
27
+ return fetch_latest_version_with_custom_registries(package_name, package_uuid) if custom_registries.any?
28
+
29
+ args = { package_name: package_name }
30
+ args[:package_uuid] = package_uuid if package_uuid
31
+
32
+ result = call_julia_helper(
33
+ function: "get_latest_version",
34
+ args: args
35
+ )
36
+
37
+ # Check if the result itself contains an error (package not found)
38
+ return nil if result["error"]
39
+
40
+ # Extract version from the result structure
41
+ # The Julia helper returns version directly in the result
42
+ return nil unless result["version"]
43
+
44
+ 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
+ end
51
+
52
+ sig { params(package_name: String, package_uuid: T.nilable(String)).returns(T.nilable(Gem::Version)) }
53
+ def fetch_latest_version_with_custom_registries(package_name, package_uuid = nil)
54
+ args = {
55
+ package_name: package_name,
56
+ package_uuid: package_uuid || "",
57
+ registry_urls: custom_registry_urls
58
+ }
59
+
60
+ result = call_julia_helper(
61
+ function: "get_latest_version_with_custom_registries",
62
+ args: args
63
+ )
64
+
65
+ # Check if the result itself contains an error (package not found)
66
+ return nil if result["error"]
67
+
68
+ # Extract version from the result structure
69
+ return nil unless result["version"]
70
+
71
+ 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
+ end
78
+
79
+ sig { params(package_name: String, package_uuid: String).returns(T.nilable(T::Hash[String, T.untyped])) }
80
+ def fetch_package_metadata(package_name, package_uuid)
81
+ call_julia_helper(
82
+ function: "get_package_metadata",
83
+ args: { package_name: package_name, package_uuid: package_uuid }
84
+ )
85
+ rescue StandardError => e
86
+ Dependabot.logger.warn("Failed to fetch metadata for #{package_name}: #{e.message}")
87
+ nil
88
+ end
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
+ sig do
109
+ params(
110
+ project_path: String,
111
+ manifest_path: T.nilable(String)
112
+ ).returns(T::Hash[String, T.untyped])
113
+ end
114
+ def parse_project(project_path:, manifest_path: nil)
115
+ args = { project_path: project_path }
116
+ args[:manifest_path] = manifest_path if manifest_path
117
+
118
+ call_julia_helper(
119
+ function: "parse_project",
120
+ args: args
121
+ )
122
+ end
123
+
124
+ sig { params(manifest_path: String).returns(T::Hash[String, T.untyped]) }
125
+ def parse_manifest(manifest_path)
126
+ call_julia_helper(
127
+ function: "parse_manifest",
128
+ args: { manifest_path: manifest_path }
129
+ )
130
+ end
131
+
132
+ sig do
133
+ params(
134
+ manifest_path: String,
135
+ name: String,
136
+ uuid: String
137
+ ).returns(T.nilable(String))
138
+ end
139
+ def get_version_from_manifest(manifest_path, name, uuid)
140
+ result = call_julia_helper(
141
+ function: "get_version_from_manifest",
142
+ args: {
143
+ manifest_path: manifest_path,
144
+ name: name,
145
+ uuid: uuid
146
+ }
147
+ )
148
+
149
+ result["version"] unless result["error"]
150
+ end
151
+
152
+ sig { params(package_name: String, package_uuid: T.nilable(String)).returns(T::Hash[String, T.untyped]) }
153
+ def find_package_source_url(package_name, package_uuid = nil)
154
+ args = { package_name: package_name }
155
+ args[:package_uuid] = package_uuid if package_uuid
156
+
157
+ call_julia_helper(
158
+ function: "find_package_source_url",
159
+ args: args
160
+ )
161
+ end
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
+ sig do
180
+ params(
181
+ project_path: String,
182
+ updates: T::Hash[String, String]
183
+ ).returns(T::Hash[String, T.untyped])
184
+ end
185
+ def update_manifest(project_path:, updates:)
186
+ call_julia_helper(
187
+ function: "update_manifest",
188
+ args: {
189
+ project_path: project_path,
190
+ updates: updates
191
+ }
192
+ )
193
+ end
194
+
195
+ sig { params(package_name: String, version: String, package_uuid: T.nilable(String)).returns(T.nilable(Time)) }
196
+ def fetch_version_release_date(package_name, version, package_uuid = nil)
197
+ result = call_julia_helper(
198
+ function: "get_version_release_date",
199
+ args: {
200
+ package_name: package_name,
201
+ version: version,
202
+ package_uuid: package_uuid
203
+ }
204
+ )
205
+
206
+ # Check if the result contains an error
207
+ return nil if result["error"]
208
+
209
+ # Parse the release date if available
210
+ return nil unless result["release_date"]
211
+
212
+ Time.parse(result["release_date"])
213
+ rescue StandardError => e
214
+ Dependabot.logger.warn("Failed to fetch release date for #{package_name} v#{version}: #{e.message}")
215
+ nil
216
+ end
217
+
218
+ sig { params(package_name: String, package_uuid: T.nilable(String)).returns(T::Array[String]) }
219
+ def fetch_available_versions(package_name, package_uuid = nil)
220
+ # Use custom registries if available
221
+ return fetch_available_versions_with_custom_registries(package_name, package_uuid) if custom_registries.any?
222
+
223
+ args = { package_name: package_name }
224
+ args[:package_uuid] = package_uuid if package_uuid
225
+
226
+ result = call_julia_helper(
227
+ function: "get_available_versions",
228
+ args: args
229
+ )
230
+
231
+ # Check if the result contains an error
232
+ return [] if result["error"]
233
+
234
+ # Extract versions array from the result
235
+ versions = result["versions"]
236
+ return [] unless versions.is_a?(Array)
237
+
238
+ versions.map(&:to_s)
239
+ rescue StandardError => e
240
+ Dependabot.logger.warn("Failed to fetch available versions for #{package_name}: #{e.message}")
241
+ []
242
+ end
243
+
244
+ sig { params(package_name: String, package_uuid: T.nilable(String)).returns(T::Array[String]) }
245
+ def fetch_available_versions_with_custom_registries(package_name, package_uuid = nil)
246
+ args = {
247
+ package_name: package_name,
248
+ package_uuid: package_uuid || "",
249
+ registry_urls: custom_registry_urls
250
+ }
251
+
252
+ result = call_julia_helper(
253
+ function: "get_available_versions_with_custom_registries",
254
+ args: args
255
+ )
256
+
257
+ # Check if the result contains an error
258
+ return [] if result["error"]
259
+
260
+ # Extract versions array from the result
261
+ versions = result["versions"]
262
+ return [] unless versions.is_a?(Array)
263
+
264
+ versions.map(&:to_s)
265
+ rescue StandardError => e
266
+ Dependabot.logger.warn(
267
+ "Failed to fetch available versions with custom registries for #{package_name}: #{e.message}"
268
+ )
269
+ []
270
+ end
271
+
272
+ private
273
+
274
+ sig { returns(T::Array[T::Hash[Symbol, String]]) }
275
+ attr_reader :custom_registries
276
+
277
+ sig { returns(T::Array[String]) }
278
+ def custom_registry_urls
279
+ custom_registries.filter_map { |reg| reg[:url] }
280
+ end
281
+
282
+ sig { returns(T::Array[Dependabot::Credential]) }
283
+ attr_reader :credentials
284
+
285
+ sig do
286
+ params(
287
+ function: String,
288
+ args: T::Hash[Symbol, T.untyped]
289
+ ).returns(T::Hash[String, T.untyped])
290
+ end
291
+ def call_julia_helper(function:, args:)
292
+ # Use the main julia helpers directory as project (contains Project.toml with DependabotHelper in [sources])
293
+ julia_project_dir = File.dirname(julia_helper_script)
294
+ julia_command = "julia --project=#{julia_project_dir} #{julia_helper_script}"
295
+
296
+ SharedHelpers.run_helper_subprocess(
297
+ command: julia_command,
298
+ function: function,
299
+ args: args,
300
+ env: julia_env,
301
+ allow_unsafe_shell_command: true
302
+ )
303
+ end
304
+
305
+ sig { returns(String) }
306
+ def julia_helper_script
307
+ # Use environment variable if available, otherwise fall back to relative path
308
+ helpers_path = ENV.fetch("DEPENDABOT_NATIVE_HELPERS_PATH", nil)
309
+ if helpers_path
310
+ File.join(helpers_path, "julia", "run_dependabot_helper.jl")
311
+ else
312
+ # Fallback for development/test environments
313
+ File.join(__dir__, "..", "..", "..", "helpers", "run_dependabot_helper.jl")
314
+ end
315
+ end
316
+
317
+ sig { returns(T::Hash[String, String]) }
318
+ def julia_env
319
+ env = {}
320
+
321
+ if ENV["DEPENDABOT_NATIVE_HELPERS_PATH"]
322
+ # In production/CI, use the shared depot where packages were precompiled
323
+ user_depot = File.join(ENV.fetch("HOME", "/home/dependabot"), ".julia")
324
+ # Trailing : is intentional. It automatically includes the bundled stdlibs
325
+ env["JULIA_DEPOT_PATH"] = "#{user_depot}:"
326
+ end
327
+ # In development use the default Julia depot
328
+
329
+ # Add Julia-specific environment variables for registry authentication
330
+ julia_credentials = credentials.select { |c| c["type"] == "julia_registry" }
331
+ julia_credentials.each_with_index do |cred, index|
332
+ env["JULIA_PKG_SERVER_REGISTRY_PREFERENCE_#{index}"] = cred.fetch("url")
333
+ env["JULIA_PKG_SERVER_#{index}_TOKEN"] = cred.fetch("token") if cred["token"]
334
+ end
335
+
336
+ env
337
+ end
338
+ end
339
+ end
340
+ end