dependabot-julia 0.398.0 → 0.399.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 91b67df0bf6edaecd8f73982a91d18d9bf7f1a47d8709b1c00b10dd8db414970
4
- data.tar.gz: 5c23460a41984bd7bc0e1614cf4ec2dbe52a10bdc2bde328ff169ca75ef8e486
3
+ metadata.gz: 6c3e654c12908a919461515b6c592e15e2382f106f203b0def25ed45bc68205b
4
+ data.tar.gz: b9c55d333c085e4cd51fd74d88e33fd98c8d379ea41be890fcf83c561f4c06ba
5
5
  SHA512:
6
- metadata.gz: aedc088fe9f5a59c56640d3260e67d0004093510f0933c183060b265a4d1550a7e072ccbb7bb7ca7a1af0cc081858ad8e99a4b11b504958ca188bd556ac13841
7
- data.tar.gz: 9c3c6c62bac976740ea9871380d9ac00d5d8fee5bb54c42f79fbfc273f233d17dff453441fdd372270a1aae64cb22e95559171573fac5a297780b669f7f36169
6
+ metadata.gz: 3a1cdb18bb53b82740729fd4ce2c8276457dd0ac796f38263844fc5ec6e20a36c84d9d2882f04d0834d006080136b5f5b457eed630ad9434625912a174362baf
7
+ data.tar.gz: a74b876e844080eb47dad827cef47cef9c861e999e8afcc013da8d12206a6a8e2131fc5810af4e82260f38c179b053d3dae0b64c2badb8bfb3ba3af3c9950f19
@@ -34,19 +34,47 @@ module Dependabot
34
34
  def fetch_files_using_julia_helper(temp_dir)
35
35
  workspace_info = registry_client.find_workspace_project_files(temp_dir.to_s)
36
36
  if workspace_info.is_a?(Dependabot::Julia::RegistryClient::Result::Failure)
37
- raise Dependabot::DependencyFileNotFound, "No Project.toml or JuliaProject.toml found."
37
+ raise_workspace_failure(workspace_info, temp_dir.to_s)
38
38
  end
39
39
 
40
40
  project_files = workspace_info.project_files
41
- manifest_path = workspace_info.manifest_file
41
+ manifest_paths = workspace_info.manifest_files
42
+ manifest_paths = [workspace_info.manifest_file] if manifest_paths.empty?
42
43
 
43
44
  fetched_files = fetch_all_project_files(project_files, temp_dir.to_s)
44
45
  raise Dependabot::DependencyFileNotFound, "No Project.toml or JuliaProject.toml found." if fetched_files.empty?
45
46
 
46
- fetch_manifest_file(fetched_files, manifest_path, project_files, temp_dir.to_s)
47
+ manifest_paths.each do |manifest_path|
48
+ fetch_manifest_file(fetched_files, manifest_path, project_files, temp_dir.to_s)
49
+ end
47
50
  fetched_files
48
51
  end
49
52
 
53
+ # Pkg refuses a project it cannot read (a [compat] entry for a package
54
+ # in no dependency section, say), so report its reason against the
55
+ # project file rather than claiming there is none
56
+ sig do
57
+ params(failure: Dependabot::Julia::RegistryClient::Result::Failure, temp_dir: String).returns(T.noreturn)
58
+ end
59
+ def raise_workspace_failure(failure, temp_dir)
60
+ project_name = %w(JuliaProject.toml Project.toml).find { |name| File.exist?(File.join(temp_dir, name)) }
61
+ raise Dependabot::DependencyFileNotFound, "No Project.toml or JuliaProject.toml found." unless project_name
62
+
63
+ raise Dependabot::DependencyFileNotParseable.new(
64
+ File.join(directory, project_name),
65
+ relative_to(failure.message, temp_dir)
66
+ )
67
+ end
68
+
69
+ # Pkg may report the temporary directory by its realpath
70
+ # ("/private/tmp/..." on macOS); strip that first so the shorter form
71
+ # cannot match inside it
72
+ sig { params(message: String, dir: String).returns(String) }
73
+ def relative_to(message, dir)
74
+ prefixes = [File.exist?(dir) ? File.realpath(dir) : dir, dir].uniq.map { |prefix| File.join(prefix, "") }
75
+ prefixes.reduce(message) { |text, prefix| text.gsub(prefix, "") }
76
+ end
77
+
50
78
  sig { params(project_files: T::Array[String], base_dir: String).returns(T::Array[Dependabot::DependencyFile]) }
51
79
  def fetch_all_project_files(project_files, base_dir)
52
80
  project_files.filter_map do |project_path|
@@ -130,6 +130,14 @@ module Dependabot
130
130
  dependencies_map,
131
131
  workspace_package_uuids
132
132
  )
133
+
134
+ merge_dependencies_from_list(
135
+ result.extra_dependencies,
136
+ ["extras"],
137
+ proj_file.name,
138
+ dependencies_map,
139
+ workspace_package_uuids
140
+ )
133
141
  end
134
142
 
135
143
  apply_manifest_versions(dependencies_map)
@@ -150,6 +158,12 @@ module Dependabot
150
158
  # entry is maintained
151
159
  next dep if dep.metadata.key?(:julia_stdlib_versions)
152
160
 
161
+ # A weakdep or extra found in the manifest is there as an indirect
162
+ # dependency of something else; the helper only bumps [deps]
163
+ # entries, so a version would announce a manifest update that never
164
+ # happens. It gets compat updates only.
165
+ next dep unless direct_dependency?(dep)
166
+
153
167
  uuid = T.cast(dep.metadata[:julia_uuid], T.nilable(String))
154
168
  version = uuid && versions[uuid]
155
169
  next dep unless version
@@ -164,22 +178,34 @@ module Dependabot
164
178
  end
165
179
  end
166
180
 
181
+ sig { params(dep: Dependabot::Dependency).returns(T::Boolean) }
182
+ def direct_dependency?(dep)
183
+ dep.requirements.any? { |req| req.groups&.include?("deps") }
184
+ end
185
+
186
+ # An environment can have a manifest per Julia release (Manifest-v1.12.toml
187
+ # beside Manifest.toml). The oldest version across them is the current one,
188
+ # so a manifest that lags behind the others still gets updated.
167
189
  sig { returns(T::Hash[String, String]) }
168
190
  def manifest_versions_by_uuid
169
- manifest = manifest_file
170
- return {} unless manifest
191
+ manifest_files.each_with_object(T.let({}, T::Hash[String, String])) do |manifest, map|
192
+ result = parse_manifest_content(T.must(manifest.content))
171
193
 
172
- result = parse_manifest_content(T.must(manifest.content))
194
+ if result.is_a?(Dependabot::Julia::RegistryClient::Result::Failure)
195
+ Dependabot.logger.warn("Failed to parse Julia manifest #{manifest.name}: #{result.message}")
196
+ next
197
+ end
173
198
 
174
- if result.is_a?(Dependabot::Julia::RegistryClient::Result::Failure)
175
- Dependabot.logger.warn("Failed to parse Julia manifest: #{result.message}")
176
- return {}
177
- end
199
+ result.dependencies.each do |dependency|
200
+ next if dependency.version.empty?
178
201
 
179
- result.dependencies.each_with_object({}) do |dependency, map|
180
- next if dependency.version.empty?
202
+ current = map[dependency.uuid]
203
+ if current && Dependabot::Julia::Version.new(current) <= Dependabot::Julia::Version.new(dependency.version)
204
+ next
205
+ end
181
206
 
182
- map[dependency.uuid] = dependency.version
207
+ map[dependency.uuid] = dependency.version
208
+ end
183
209
  end
184
210
  end
185
211
 
@@ -201,9 +227,9 @@ module Dependabot
201
227
  end
202
228
  end
203
229
 
204
- sig { returns(T.nilable(Dependabot::DependencyFile)) }
205
- def manifest_file
206
- dependency_files.find do |f|
230
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
231
+ def manifest_files
232
+ dependency_files.select do |f|
207
233
  File.basename(f.name).match?(/^(Julia)?Manifest(?:-v[\d.]+)?\.toml$/i)
208
234
  end
209
235
  end
@@ -250,7 +276,7 @@ module Dependabot
250
276
  uuid = dependency.uuid
251
277
  requirement_string = dependency.requirement
252
278
 
253
- next if skip_dependency?(dependency, file_name, workspace_package_uuids)
279
+ next if skip_dependency?(dependency, groups, file_name, workspace_package_uuids)
254
280
 
255
281
  new_requirement = {
256
282
  requirement: requirement_string,
@@ -330,13 +356,19 @@ module Dependabot
330
356
  sig do
331
357
  params(
332
358
  dependency: Dependabot::Julia::RegistryClient::Result::ProjectDependency,
359
+ groups: T::Array[String],
333
360
  file_name: String,
334
361
  workspace_package_uuids: T::Array[String]
335
362
  ).returns(T::Boolean)
336
363
  end
337
- def skip_dependency?(dependency, file_name, workspace_package_uuids)
364
+ def skip_dependency?(dependency, groups, file_name, workspace_package_uuids)
338
365
  return true if workspace_package_uuids.include?(dependency.uuid)
339
366
 
367
+ # A test dependency under [extras] is only maintained once the project
368
+ # has given it a compat entry, as CompatHelper does by default
369
+ # (IfExistingCompatExtras); nothing is synthesized for the rest.
370
+ return true if dependency.requirement.nil? && groups.include?("extras")
371
+
340
372
  # Pkg pins a standard library to the version bundled with Julia, so
341
373
  # a compat entry tracking its registry releases (a legacy bridge for
342
374
  # older Julia, or an "upgradable" stdlib release) can make the
@@ -58,23 +58,25 @@ module Dependabot
58
58
  SharedHelpers.in_a_temporary_repo_directory(T.must(dependency_files.first).directory, repo_contents_path) do
59
59
  # Update all project files (main + workspace members)
60
60
  updated_project_files = update_all_project_files
61
- actual_manifest = find_manifest_file
61
+ manifests = find_manifest_files
62
62
 
63
- return all_projects_only_update(updated_project_files) if actual_manifest.nil?
63
+ return all_projects_only_update(updated_project_files) if manifests.empty?
64
64
 
65
65
  # Requirement-only updates (no target versions) have nothing to tell
66
66
  # Pkg; ship the Project.toml changes on their own.
67
67
  return all_projects_only_update(updated_project_files) if build_updates_hash.empty?
68
68
 
69
69
  # Write all updated project files to disk for Julia's Pkg
70
- write_all_temporary_files(updated_project_files, actual_manifest)
71
- result = call_julia_helper
70
+ write_all_temporary_files(updated_project_files, manifests)
71
+ updated_files.concat(all_projects_only_update(updated_project_files))
72
72
 
73
- if result.is_a?(Dependabot::Julia::RegistryClient::Result::Failure)
74
- return handle_julia_helper_error_multi(result, actual_manifest, updated_project_files)
73
+ # Each manifest is resolved separately, by the Julia release that uses it
74
+ manifests.each do |manifest|
75
+ updated_manifest = updated_manifest_file(manifest)
76
+ updated_files << updated_manifest if updated_manifest
75
77
  end
76
-
77
- build_updated_files_multi(updated_files, updated_project_files, actual_manifest, result)
78
+ # A manifest that could not be resolved is reported in a notice instead
79
+ return updated_files if updated_files.empty? && notices.any?
78
80
  end
79
81
 
80
82
  raise "No files changed!" if updated_files.empty?
@@ -129,10 +131,10 @@ module Dependabot
129
131
  sig do
130
132
  params(
131
133
  updated_project_files: T::Array[T::Hash[Symbol, T.untyped]],
132
- actual_manifest: Dependabot::DependencyFile
134
+ manifests: T::Array[Dependabot::DependencyFile]
133
135
  ).void
134
136
  end
135
- def write_all_temporary_files(updated_project_files, actual_manifest)
137
+ def write_all_temporary_files(updated_project_files, manifests)
136
138
  # Write all updated project files
137
139
  updated_project_files.each do |update_info|
138
140
  file = T.cast(update_info[:file], Dependabot::DependencyFile)
@@ -143,45 +145,41 @@ module Dependabot
143
145
  File.write(file_path, content)
144
146
  end
145
147
 
146
- # Write manifest file
147
- manifest_path = actual_manifest.name
148
- FileUtils.mkdir_p(File.dirname(manifest_path)) if manifest_path.include?("/")
149
- File.write(manifest_path, actual_manifest.content)
148
+ manifests.each do |manifest|
149
+ FileUtils.mkdir_p(File.dirname(manifest.name)) if manifest.name.include?("/")
150
+ File.write(manifest.name, manifest.content)
151
+ end
150
152
  end
151
153
 
152
- sig do
153
- returns(
154
- T.any(
155
- Dependabot::Julia::RegistryClient::Result::ManifestUpdate,
156
- Dependabot::Julia::RegistryClient::Result::Failure
157
- )
158
- )
159
- end
160
- def call_julia_helper
161
- registry_client.update_manifest(
154
+ sig { params(manifest: Dependabot::DependencyFile).returns(T.nilable(Dependabot::DependencyFile)) }
155
+ def updated_manifest_file(manifest)
156
+ result = registry_client.update_manifest(
162
157
  project_path: Dir.pwd,
158
+ manifest_path: File.expand_path(manifest.name),
163
159
  updates: build_updates_hash
164
160
  )
161
+
162
+ if result.is_a?(Dependabot::Julia::RegistryClient::Result::Failure)
163
+ handle_julia_helper_error(result, manifest)
164
+ return nil
165
+ end
166
+ return nil if result.manifest_content == manifest.content
167
+
168
+ updated_file(file: manifest_file_for_path(result.manifest_path), content: result.manifest_content)
165
169
  end
166
170
 
167
171
  sig do
168
172
  params(
169
173
  result: Dependabot::Julia::RegistryClient::Result::Failure,
170
- actual_manifest: Dependabot::DependencyFile,
171
- updated_project_files: T::Array[T::Hash[Symbol, T.untyped]]
172
- ).returns(T::Array[Dependabot::DependencyFile])
174
+ manifest: Dependabot::DependencyFile
175
+ ).void
173
176
  end
174
- def handle_julia_helper_error_multi(result, actual_manifest, updated_project_files)
177
+ def handle_julia_helper_error(result, manifest)
175
178
  error_message = result.message
176
- manifest_path = actual_manifest.name
177
-
178
- is_resolver_error = resolver_error?(error_message)
179
- raise error_message unless is_resolver_error
180
-
181
- add_manifest_update_notice(manifest_path, error_message)
179
+ raise error_message unless resolver_error?(error_message)
182
180
 
183
- # Return all updated Project.toml files
184
- all_projects_only_update(updated_project_files)
181
+ # The Project.toml changes still go out without this manifest
182
+ add_manifest_update_notice(manifest.name, error_message)
185
183
  end
186
184
 
187
185
  sig { params(error_message: String).returns(T::Boolean) }
@@ -216,31 +214,6 @@ module Dependabot
216
214
  )
217
215
  end
218
216
 
219
- sig do
220
- params(
221
- updated_files: T::Array[Dependabot::DependencyFile],
222
- updated_project_files: T::Array[T::Hash[Symbol, T.untyped]],
223
- actual_manifest: Dependabot::DependencyFile,
224
- result: Dependabot::Julia::RegistryClient::Result::ManifestUpdate
225
- ).void
226
- end
227
- def build_updated_files_multi(updated_files, updated_project_files, actual_manifest, result)
228
- # Add all updated project files
229
- updated_project_files.each do |update_info|
230
- file = T.cast(update_info[:file], Dependabot::DependencyFile)
231
- content = T.cast(update_info[:content], String)
232
- next if content == file.content
233
-
234
- updated_files << updated_file(file: file, content: content)
235
- end
236
-
237
- updated_manifest_content = result.manifest_content
238
- return unless updated_manifest_content != actual_manifest.content
239
-
240
- manifest_for_update = manifest_file_for_path(result.manifest_path)
241
- updated_files << updated_file(file: manifest_for_update, content: updated_manifest_content)
242
- end
243
-
244
217
  private
245
218
 
246
219
  sig { returns(T::Hash[String, T::Hash[String, String]]) }
@@ -251,6 +224,10 @@ module Dependabot
251
224
  dependencies.each do |dependency|
252
225
  version = dependency.version
253
226
  next unless version
227
+ # Only a [deps] entry has a manifest entry of its own to bump;
228
+ # the helper leaves weakdeps and extras alone, so their updates
229
+ # are compat-only and need no Pkg run
230
+ next unless dependency.requirements.any? { |req| req.groups&.include?("deps") }
254
231
 
255
232
  uuid = dependency.metadata[:julia_uuid]
256
233
  unless uuid.is_a?(String)
@@ -294,15 +271,15 @@ module Dependabot
294
271
  )
295
272
  end
296
273
 
297
- sig { returns(T.nilable(Dependabot::DependencyFile)) }
298
- def find_manifest_file
299
- # The file fetcher has already identified the correct manifest file
300
- # For regular packages: manifest in same directory
301
- # For workspace packages: manifest in parent directory
302
- # We just need to find it in dependency_files
274
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
275
+ def find_manifest_files
276
+ # The file fetcher has already identified the environment's manifests
277
+ # For regular packages: manifests in same directory
278
+ # For workspace packages: manifests in parent directory
279
+ # We just need to find them in dependency_files
303
280
  project_dir = T.must(project_file).directory
304
281
 
305
- dependency_files.find do |f|
282
+ dependency_files.select do |f|
306
283
  # Use basename to get just the filename, not the full path with ../
307
284
  is_manifest = File.basename(f.name).match?(/^(Julia)?Manifest(?:-v[\d.]+)?\.toml$/i)
308
285
  is_manifest && (f.directory == project_dir || parent_directory_of?(f.directory, project_dir))
@@ -14,6 +14,7 @@ module Dependabot
14
14
  extend T::Sig
15
15
 
16
16
  PACKAGE_LANGUAGE = "julia"
17
+ RELEASE_DATE_PENDING = "release_date_pending"
17
18
 
18
19
  sig do
19
20
  params(
@@ -73,9 +74,14 @@ module Dependabot
73
74
 
74
75
  available_versions.map do |version_string|
75
76
  version = Julia::Version.new(version_string)
76
- release_date = release_dates[version_string]
77
+ date_result = release_dates[version_string]
78
+ date_result = nil if date_result.is_a?(RegistryClient::Result::Failure)
77
79
 
78
- create_package_release(version, release_date)
80
+ create_package_release(
81
+ version,
82
+ convert_single_date(date_result&.release_date),
83
+ release_date_pending: date_result&.pending || false
84
+ )
79
85
  end
80
86
  end
81
87
 
@@ -84,7 +90,9 @@ module Dependabot
84
90
  registry_client: RegistryClient,
85
91
  available_versions: T::Array[String],
86
92
  uuid: T.nilable(String)
87
- ).returns(T::Hash[String, T.nilable(Time)])
93
+ ).returns(
94
+ T::Hash[String, T.any(RegistryClient::Result::ReleaseDate, RegistryClient::Result::Failure)]
95
+ )
88
96
  end
89
97
  def fetch_release_dates_batch(registry_client, available_versions, uuid)
90
98
  return {} if available_versions.empty?
@@ -103,20 +111,7 @@ module Dependabot
103
111
  dates_for_package = result.packages[dependency.name]
104
112
  return {} unless dates_for_package.is_a?(RegistryClient::Result::ReleaseDates)
105
113
 
106
- convert_dates_to_time_objects(dates_for_package)
107
- end
108
-
109
- sig do
110
- params(
111
- release_dates: RegistryClient::Result::ReleaseDates
112
- ).returns(T::Hash[String, T.nilable(Time)])
113
- end
114
- def convert_dates_to_time_objects(release_dates)
115
- release_dates.dates.transform_values do |result|
116
- next if result.is_a?(RegistryClient::Result::Failure)
117
-
118
- convert_single_date(result.release_date)
119
- end
114
+ dates_for_package.dates
120
115
  end
121
116
 
122
117
  sig { params(date_value: T.nilable(String)).returns(T.nilable(Time)) }
@@ -131,16 +126,18 @@ module Dependabot
131
126
  sig do
132
127
  params(
133
128
  version: Julia::Version,
134
- release_date: T.nilable(Time)
129
+ release_date: T.nilable(Time),
130
+ release_date_pending: T::Boolean
135
131
  ).returns(Dependabot::Package::PackageRelease)
136
132
  end
137
- def create_package_release(version, release_date)
133
+ def create_package_release(version, release_date, release_date_pending:)
138
134
  Dependabot::Package::PackageRelease.new(
139
135
  version: version,
140
136
  released_at: release_date,
141
137
  latest: false, # Will be determined later
142
138
  yanked: false, # Yanked versions are filtered out by the Julia registry helper
143
- language: Dependabot::Package::PackageLanguage.new(name: PACKAGE_LANGUAGE)
139
+ language: Dependabot::Package::PackageLanguage.new(name: PACKAGE_LANGUAGE),
140
+ details: release_date_pending ? { RELEASE_DATE_PENDING => true } : {}
144
141
  )
145
142
  end
146
143
 
@@ -214,6 +214,7 @@ module Dependabot
214
214
  const :julia_version, String
215
215
  const :dependencies, T::Array[ProjectDependency]
216
216
  const :weak_dependencies, T::Array[ProjectDependency]
217
+ const :extra_dependencies, T::Array[ProjectDependency]
217
218
  const :project_path, String
218
219
 
219
220
  sig { params(value: Object).returns(T.any(Project, Failure)) }
@@ -230,6 +231,7 @@ module Dependabot
230
231
  julia_version: ValueParser.string(hash, "julia_version", context),
231
232
  dependencies: parse_dependencies(hash, "dependencies", context),
232
233
  weak_dependencies: parse_dependencies(hash, "weak_dependencies", context),
234
+ extra_dependencies: parse_dependencies(hash, "extra_dependencies", context),
233
235
  project_path: ValueParser.string(hash, "project_path", context)
234
236
  )
235
237
  end
@@ -336,6 +338,8 @@ module Dependabot
336
338
 
337
339
  const :project_files, T::Array[String]
338
340
  const :manifest_file, String
341
+ # Every manifest of the environment, including version-specific ones
342
+ const :manifest_files, T::Array[String], default: []
339
343
  const :workspace_root, String
340
344
 
341
345
  sig { params(value: Object).returns(T.any(WorkspaceFiles, Failure)) }
@@ -351,6 +355,7 @@ module Dependabot
351
355
  "#{context} project_files"
352
356
  ),
353
357
  manifest_file: ValueParser.string(hash, "manifest_file", context),
358
+ manifest_files: ValueParser.optional_string_array(hash, "manifest_files", context),
354
359
  workspace_root: ValueParser.string(hash, "workspace_root", context)
355
360
  )
356
361
  end
@@ -429,6 +434,8 @@ module Dependabot
429
434
  extend T::Sig
430
435
 
431
436
  const :release_date, T.nilable(String)
437
+ # Registered in General after the last GeneralMetadata.jl build, so no date is published yet
438
+ const :pending, T::Boolean, default: false
432
439
 
433
440
  sig { params(value: Object, context: String).returns(T.any(ReleaseDate, Failure)) }
434
441
  def self.from_object(value, context:)
@@ -436,7 +443,10 @@ module Dependabot
436
443
  failure = Result.failure_from(hash, context)
437
444
  return failure if failure
438
445
 
439
- new(release_date: ValueParser.nilable_string(hash, "release_date", context))
446
+ new(
447
+ release_date: ValueParser.nilable_string(hash, "release_date", context),
448
+ pending: ValueParser.optional_boolean(hash, "release_date_pending", context)
449
+ )
440
450
  end
441
451
 
442
452
  sig { params(value: Object, context: String).returns(T.any(ReleaseDate, Failure)) }
@@ -178,15 +178,17 @@ module Dependabot
178
178
  sig do
179
179
  params(
180
180
  project_path: String,
181
- updates: T::Hash[String, T::Hash[String, String]]
181
+ updates: T::Hash[String, T::Hash[String, String]],
182
+ manifest_path: T.nilable(String)
182
183
  ).returns(T.any(Result::ManifestUpdate, Result::Failure))
183
184
  end
184
- def update_manifest(project_path:, updates:)
185
+ def update_manifest(project_path:, updates:, manifest_path: nil)
185
186
  Result::ManifestUpdate.from_object(
186
187
  call_julia_helper(
187
188
  function: "update_manifest",
188
189
  args: {
189
190
  project_path: project_path,
191
+ manifest_path: manifest_path,
190
192
  updates: updates
191
193
  }
192
194
  )
@@ -41,8 +41,11 @@ module Dependabot
41
41
  # intersection semantics for operator-style lists anyway because this
42
42
  # method also parses Dependabot ignore conditions (e.g. ">= 2.a, < 3"),
43
43
  # which are always intersections; treating those as unions would make
44
- # every ignore condition match all versions.
44
+ # every ignore condition match all versions. A list of equalities only
45
+ # ("=0.5.4, =0.5.5") would admit nothing as an intersection, so it is
46
+ # read as the union Pkg takes.
45
47
  return false if constraints.length <= 1
48
+ return false if constraints.all? { |c| c.start_with?("=") }
46
49
 
47
50
  constraints.all? { |c| c.match?(/^[<>=]/) }
48
51
  end
@@ -65,6 +68,18 @@ module Dependabot
65
68
  requirements_array(requirement_string)
66
69
  end
67
70
 
71
+ # Whether a compat entry admits the version. Pkg compares a bound
72
+ # against major.minor.patch only, so a JLL rebuild ("0.0.43+1") is
73
+ # admitted by "=0.0.43" or "0.0.42 - 0.0.43" exactly as the version it
74
+ # rebuilds is, and a prerelease exactly as its release ("1" rejects
75
+ # "2.0.0-rc1"). Ignore conditions go through satisfied_by? instead and
76
+ # keep the ordering between builds ("> 1.6.10" ignores "1.6.10+1").
77
+ sig { params(version: T.any(Gem::Version, String)).returns(T::Boolean) }
78
+ def admits?(version)
79
+ release = Dependabot::Julia::Version.new(version.to_s).compat_version_string
80
+ T.cast(satisfied_by?(Dependabot::Julia::Version.new(release)), T::Boolean)
81
+ end
82
+
68
83
  sig { params(version: String).returns(String) }
69
84
  def self.normalize_version(version)
70
85
  # Remove 'v' prefix if present (common in Julia)
@@ -55,6 +55,13 @@ module Dependabot
55
55
  @available_versions ||= T.let(fetch_available_versions, T.nilable(T::Array[Gem::Version]))
56
56
  end
57
57
 
58
+ # Lowest non-vulnerable version above the current one. Like
59
+ # Package::PackageLatestVersionFinder, security fixes skip the cooldown.
60
+ sig { returns(T.nilable(Gem::Version)) }
61
+ def lowest_security_fix_version
62
+ @lowest_security_fix_version ||= T.let(fetch_lowest_security_fix_version, T.nilable(Gem::Version))
63
+ end
64
+
58
65
  private
59
66
 
60
67
  sig { returns(Dependabot::Dependency) }
@@ -81,16 +88,29 @@ module Dependabot
81
88
  sig { returns(T::Array[T::Hash[Symbol, String]]) }
82
89
  attr_reader :custom_registries
83
90
 
84
- sig { returns(T::Array[Gem::Version]) }
85
- def fetch_available_versions
86
- # Fetch all package releases using the PackageDetailsFetcher
87
- package_fetcher = Julia::Package::PackageDetailsFetcher.new(
88
- dependency: dependency,
89
- credentials: credentials,
90
- custom_registries: custom_registries
91
+ sig { returns(T::Array[Dependabot::Package::PackageRelease]) }
92
+ def package_releases
93
+ @package_releases ||= T.let(
94
+ Julia::Package::PackageDetailsFetcher.new(
95
+ dependency: dependency,
96
+ credentials: credentials,
97
+ custom_registries: custom_registries
98
+ ).fetch_package_releases,
99
+ T.nilable(T::Array[Dependabot::Package::PackageRelease])
91
100
  )
101
+ end
92
102
 
93
- releases = package_fetcher.fetch_package_releases
103
+ sig { returns(T.nilable(Gem::Version)) }
104
+ def fetch_lowest_security_fix_version
105
+ versions = filter_prerelease_versions(package_releases.map(&:version).sort)
106
+ versions = filter_ignored_versions(versions)
107
+ versions = filter_lower_versions(versions)
108
+ Dependabot::UpdateCheckers::VersionFilters.filter_vulnerable_versions(versions, security_advisories).min
109
+ end
110
+
111
+ sig { returns(T::Array[Gem::Version]) }
112
+ def fetch_available_versions
113
+ releases = package_releases
94
114
  return [] if releases.empty?
95
115
 
96
116
  # Filter releases based on cooldown
@@ -110,8 +130,10 @@ module Dependabot
110
130
  versions = filter_ignored_versions(versions)
111
131
  return [] if versions.empty?
112
132
 
113
- # Filter out lower versions
114
- versions = filter_lower_versions(versions)
133
+ # Filter out lower versions, keeping the current release so that an
134
+ # up-to-date dependency reports it as the latest: the base class takes
135
+ # a nil latest_version as out of date and goes on to look for unlocks
136
+ versions = filter_lower_versions(versions, keep_current: true)
115
137
  return [] if versions.empty?
116
138
 
117
139
  # Filter out vulnerable versions
@@ -168,12 +190,12 @@ module Dependabot
168
190
  filtered
169
191
  end
170
192
 
171
- sig { params(versions: T::Array[Gem::Version]).returns(T::Array[Gem::Version]) }
172
- def filter_lower_versions(versions)
193
+ sig { params(versions: T::Array[Gem::Version], keep_current: T::Boolean).returns(T::Array[Gem::Version]) }
194
+ def filter_lower_versions(versions, keep_current: false)
173
195
  return versions unless dependency.version
174
196
 
175
197
  current_version = Dependabot::Julia::Version.new(dependency.version)
176
- versions.select { |v| v > current_version }
198
+ versions.select { |v| v > current_version || (keep_current && v == current_version) }
177
199
  end
178
200
 
179
201
  sig { returns(T::Array[Dependabot::Requirement]) }
@@ -187,6 +209,8 @@ module Dependabot
187
209
  def cooldown_active_for_release?(release)
188
210
  cooldown_days = determine_cooldown_days(release.version)
189
211
  return false unless cooldown_days&.positive?
212
+ # A General release too new to have a published date is at most about a day old
213
+ return true if release.details[Julia::Package::PackageDetailsFetcher::RELEASE_DATE_PENDING]
190
214
  return false unless release.released_at
191
215
 
192
216
  # Check if enough time has passed since release
@@ -225,7 +249,10 @@ module Dependabot
225
249
  return nil unless cooldown_config
226
250
 
227
251
  current_version = dependency.version ? Dependabot::Julia::Version.new(dependency.version) : nil
228
- return nil unless current_version
252
+ # Without a Manifest there is no version to measure a bump from; like
253
+ # CooldownCalculation.cooldown_days_for, fall back to default_days
254
+ # rather than skipping the cooldown
255
+ return cooldown_days_for_bump_type(:default) unless current_version
229
256
 
230
257
  version_bump_type = determine_version_bump_type(version, current_version)
231
258
  cooldown_days_for_bump_type(version_bump_type)
@@ -90,7 +90,7 @@ module Dependabot
90
90
  sig { params(requirement_string: String, versions: T::Array[Dependabot::Julia::Version]).returns(String) }
91
91
  def widened_stdlib_requirement(requirement_string, versions)
92
92
  reqs = Dependabot::Julia::Requirement.requirements_array(requirement_string)
93
- missing = versions.reject { |version| reqs.any? { |req| req.satisfied_by?(version) } }
93
+ missing = versions.reject { |version| reqs.any? { |req| req.admits?(version) } }
94
94
 
95
95
  missing.reduce(requirement_string) do |entry, version|
96
96
  append_spec_string(entry, stdlib_version_spec(version))
@@ -106,7 +106,7 @@ module Dependabot
106
106
  minor = (version.segments[1] || 0).to_i
107
107
  patch = (version.segments[2] || 0).to_i
108
108
 
109
- return "< 0.0.1" if major.zero? && minor.zero? && patch.zero?
109
+ return "<0.0.1" if major.zero? && minor.zero? && patch.zero?
110
110
  return major.to_s if major.positive? && minor.zero?
111
111
 
112
112
  simplified_version_spec(version)
@@ -121,9 +121,11 @@ module Dependabot
121
121
  def update_requirement(requirement, target_version)
122
122
  current_requirement = requirement.requirement_string
123
123
 
124
- # If requirement is nil (no compat entry), use target version
124
+ # If requirement is nil (no compat entry), use target version. A JLL's
125
+ # "1.6.10+0" has to be written as "1.6.10": Pkg rejects the build
126
+ # number in a compat entry, and the bound admits every build anyway.
125
127
  new_requirement = if current_requirement.nil?
126
- target_version.to_s
128
+ target_version.compat_version_string
127
129
  else
128
130
  updated_version_requirement(current_requirement, target_version)
129
131
  end
@@ -141,7 +143,7 @@ module Dependabot
141
143
 
142
144
  # Check if any requirement is satisfied by the target version
143
145
  # Note: This uses the implicit caret semantics from the Requirement class
144
- satisfied = reqs.any? { |req| req.satisfied_by?(target_version) }
146
+ satisfied = reqs.any? { |req| req.admits?(target_version) }
145
147
 
146
148
  case update_strategy
147
149
  when :bump_versions
@@ -163,11 +165,26 @@ module Dependabot
163
165
 
164
166
  sig { params(requirement_string: String, new_spec: String).returns(String) }
165
167
  def append_spec_string(requirement_string, new_spec)
166
- # Append the new spec to the existing requirement (CompatHelper KeepEntry behavior)
168
+ # Add the new spec to the existing requirement (CompatHelper KeepEntry behavior)
167
169
  # Detect whether the existing requirement uses spaces after commas and preserve that format
168
170
  # and default to ", " if no commas found
169
171
  separator = requirement_string.include?(",") && !requirement_string.include?(", ") ? "," : ", "
170
- "#{requirement_string}#{separator}#{new_spec}"
172
+ # Keep the entry in ascending order ("<0.0.1, 1", not "1, <0.0.1"):
173
+ # the new spec goes before the first one that starts above it
174
+ specs = requirement_string.split(",").map(&:strip)
175
+ index = specs.index { |spec| spec_lower_bound(spec) > spec_lower_bound(new_spec) }
176
+ return "#{requirement_string}#{separator}#{new_spec}" unless index
177
+
178
+ specs.insert(index, new_spec).join(separator)
179
+ end
180
+
181
+ # The lowest version a single spec in an entry admits, for ordering; an
182
+ # upper bound alone ("<0.0.1") starts at zero
183
+ sig { params(spec: String).returns(Dependabot::Julia::Version) }
184
+ def spec_lower_bound(spec)
185
+ return Dependabot::Julia::Version.new("0") if spec.start_with?("<", "≤")
186
+
187
+ Dependabot::Julia::Version.new(spec[/\d+(?:\.\d+)*/] || "0")
171
188
  end
172
189
 
173
190
  sig { params(target_version: Dependabot::Julia::Version).returns(String) }
@@ -79,6 +79,19 @@ module Dependabot
79
79
  @latest_resolvable_version ||= T.let(latest_version, T.nilable(Gem::Version))
80
80
  end
81
81
 
82
+ sig { override.returns(T.nilable(Gem::Version)) }
83
+ def lowest_security_fix_version
84
+ return nil if stdlib?
85
+
86
+ latest_version_finder.lowest_security_fix_version
87
+ end
88
+
89
+ sig { override.returns(T.nilable(Gem::Version)) }
90
+ def lowest_resolvable_security_fix_version
91
+ # Resolvability is not checked, as for latest_resolvable_version
92
+ lowest_security_fix_version
93
+ end
94
+
82
95
  sig { override.returns(T.nilable(T.any(Dependabot::Version, String))) }
83
96
  def latest_resolvable_version_with_no_unlock
84
97
  return nil if stdlib?
@@ -96,7 +109,7 @@ module Dependabot
96
109
  candidates.max
97
110
  else
98
111
  reqs = requirement_class.requirements_array(current_requirement)
99
- candidates.select { |version| reqs.any? { |req| req.satisfied_by?(version) } }.max
112
+ candidates.select { |version| reqs.any? { |req| req.admits?(version) } }.max
100
113
  end
101
114
  return nil unless best
102
115
 
@@ -107,7 +120,7 @@ module Dependabot
107
120
  def updated_requirements
108
121
  Dependabot::Julia::RequirementsUpdater.new(
109
122
  requirements: dependency.requirements,
110
- target_version: latest_resolvable_version&.to_s,
123
+ target_version: preferred_resolvable_version&.to_s,
111
124
  update_strategy: requirements_update_strategy&.to_s&.to_sym,
112
125
  stdlib_versions: stdlib_versions
113
126
  ).updated_requirements
@@ -115,6 +128,12 @@ module Dependabot
115
128
 
116
129
  private
117
130
 
131
+ sig { override.returns(T::Boolean) }
132
+ def latest_version_resolvable_with_full_unlock?
133
+ # Full unlock checks aren't implemented for Julia (yet)
134
+ false
135
+ end
136
+
118
137
  # Versions a stdlib's compat entry has to admit, by project file; set by
119
138
  # the file parser for packages that ship with any Julia release the
120
139
  # project supports
@@ -59,6 +59,14 @@ module Dependabot
59
59
  @version_string
60
60
  end
61
61
 
62
+ # The "major.minor.patch" part, which is all a [compat] entry can carry:
63
+ # Pkg rejects "1.6.10+0" or "1.6.10-rc1" as a specifier, and its bounds
64
+ # compare major, minor and patch only, so "1.6.10" admits both.
65
+ sig { returns(String) }
66
+ def compat_version_string
67
+ T.must(@version_string[/\A\d+(?:\.\d+){0,2}/])
68
+ end
69
+
62
70
  sig { override.returns(String) }
63
71
  def to_semver
64
72
  @version_string
@@ -69,5 +69,12 @@ Dependabot::PullRequestCreator::Labeler
69
69
  .register_label_details("julia", name: "julia", colour: "a270ba")
70
70
 
71
71
  require "dependabot/dependency"
72
- Dependabot::Dependency
73
- .register_production_check("julia", ->(_) { true })
72
+ # [extras] hold test-only packages; weakdeps are loaded on demand by
73
+ # extensions and count as runtime, like [deps]
74
+ Dependabot::Dependency.register_production_check(
75
+ "julia",
76
+ lambda do |groups|
77
+ groups = T.cast(groups, T::Array[String])
78
+ groups.empty? || groups.any? { |group| group != "extras" }
79
+ end
80
+ )
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.398.0
4
+ version: 0.399.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.398.0
18
+ version: 0.399.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.398.0
25
+ version: 0.399.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: debug
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -262,7 +262,7 @@ licenses:
262
262
  - MIT
263
263
  metadata:
264
264
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
265
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.398.0
265
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.399.0
266
266
  rdoc_options: []
267
267
  require_paths:
268
268
  - lib