spm_version_updates 1.1.1
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 +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +47 -0
- data/lib/spm_version_updates/allow_host_normalizer.rb +53 -0
- data/lib/spm_version_updates/credential_redactor.rb +16 -0
- data/lib/spm_version_updates/fail_on_threshold.rb +45 -0
- data/lib/spm_version_updates/git_host_normalizer.rb +63 -0
- data/lib/spm_version_updates/git_operations.rb +150 -0
- data/lib/spm_version_updates/manifest_parser.rb +258 -0
- data/lib/spm_version_updates/package_resolved.rb +46 -0
- data/lib/spm_version_updates/repository_link.rb +112 -0
- data/lib/spm_version_updates/repository_update_rules.rb +222 -0
- data/lib/spm_version_updates/semver.rb +55 -0
- data/lib/spm_version_updates/spm_checker.rb +532 -0
- data/lib/spm_version_updates/spm_package_context.rb +49 -0
- data/lib/spm_version_updates/update_severity.rb +67 -0
- data/lib/spm_version_updates/upgrade_suggestion.rb +62 -0
- data/lib/spm_version_updates/version.rb +5 -0
- data/lib/spm_version_updates/version_tag_fetcher.rb +110 -0
- data/lib/spm_version_updates/version_tags_persistent_cache.rb +99 -0
- data/lib/spm_version_updates/xcode_parser.rb +71 -0
- data/lib/spm_version_updates/xcode_project_package_reader.rb +132 -0
- data/lib/spm_version_updates.rb +21 -0
- data/spm_version_updates.gemspec +46 -0
- metadata +79 -0
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "allow_host_normalizer"
|
|
4
|
+
require_relative "credential_redactor"
|
|
5
|
+
require_relative "git_operations"
|
|
6
|
+
require_relative "manifest_parser"
|
|
7
|
+
require_relative "package_resolved"
|
|
8
|
+
require_relative "repository_update_rules"
|
|
9
|
+
require_relative "semver"
|
|
10
|
+
require_relative "spm_package_context"
|
|
11
|
+
require_relative "upgrade_suggestion"
|
|
12
|
+
require_relative "version_tag_fetcher"
|
|
13
|
+
require_relative "version_tags_persistent_cache"
|
|
14
|
+
require_relative "xcode_parser"
|
|
15
|
+
|
|
16
|
+
# Core SPM version checking logic (migrated from Danger plugin)
|
|
17
|
+
class SpmChecker
|
|
18
|
+
VERSION_TAG_WORKER_COUNT = 8
|
|
19
|
+
|
|
20
|
+
# Raised when allow-hosts blocks a repository before git is contacted.
|
|
21
|
+
class DisallowedRepositoryHost < StandardError; end
|
|
22
|
+
|
|
23
|
+
# Structured facts about each warning, used by the GitHub Action comment
|
|
24
|
+
# renderer. `check_for_updates` and `check_manifests` still return the legacy
|
|
25
|
+
# string warnings for compatibility with existing plugin-style callers.
|
|
26
|
+
attr_reader :warning_details
|
|
27
|
+
|
|
28
|
+
attr_accessor :allow_hosts,
|
|
29
|
+
:check_branches,
|
|
30
|
+
:check_revisions,
|
|
31
|
+
:check_when_exact,
|
|
32
|
+
:ignore_repos,
|
|
33
|
+
:repository_update_rules,
|
|
34
|
+
:report_above_maximum,
|
|
35
|
+
:report_pre_releases,
|
|
36
|
+
:version_tags_cache_dir,
|
|
37
|
+
:version_tags_cache_ttl_seconds
|
|
38
|
+
|
|
39
|
+
# Optional callable `(package, error)` invoked instead of raising when a git
|
|
40
|
+
# lookup fails, so callers like the Danger plugin can warn and keep checking
|
|
41
|
+
# the remaining packages. When nil (the default), lookup failures raise one
|
|
42
|
+
# combined GitOperations::LsRemoteError exactly as before.
|
|
43
|
+
attr_accessor :lookup_failure_handler
|
|
44
|
+
|
|
45
|
+
# Optional callable `(resolved_path, error)` invoked instead of raising when a
|
|
46
|
+
# Package.resolved file is malformed; the file is skipped. When nil (the
|
|
47
|
+
# default), PackageResolved::MalformedFileError is raised.
|
|
48
|
+
attr_accessor :malformed_resolved_handler
|
|
49
|
+
|
|
50
|
+
def self.redact_credentials(value)
|
|
51
|
+
CredentialRedactor.redact(value)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def initialize
|
|
55
|
+
@check_when_exact = @check_revisions = @report_above_maximum = @report_pre_releases = false
|
|
56
|
+
@check_branches = true
|
|
57
|
+
@lookup_failure_handler = @malformed_resolved_handler = nil
|
|
58
|
+
@ignore_repos = []
|
|
59
|
+
@repository_update_rules = RepositoryUpdateRules.empty
|
|
60
|
+
@allow_hosts = []
|
|
61
|
+
@warnings = []
|
|
62
|
+
@warning_details = []
|
|
63
|
+
@version_tags_cache = {}
|
|
64
|
+
@version_tag_lookup_errors = {}
|
|
65
|
+
@reported_lookup_failures = {}
|
|
66
|
+
@version_tags_cache_dir = nil
|
|
67
|
+
@version_tags_cache_ttl_seconds = VersionTagsPersistentCache::DEFAULT_TTL_SECONDS
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Check for SPM updates using an Xcode project as the source of dependencies.
|
|
71
|
+
#
|
|
72
|
+
# @param [String] xcodeproj_path The path to your Xcode project
|
|
73
|
+
# @return [Array<String>] Array of warning messages
|
|
74
|
+
def check_for_updates(xcodeproj_path)
|
|
75
|
+
clear_warnings
|
|
76
|
+
reset_version_tags_cache
|
|
77
|
+
normalize_ignore_repos
|
|
78
|
+
normalize_allow_hosts
|
|
79
|
+
|
|
80
|
+
remote_packages = XcodeParser.get_packages(xcodeproj_path)
|
|
81
|
+
resolved_versions = XcodeParser.get_resolved_versions(xcodeproj_path, &@malformed_resolved_handler)
|
|
82
|
+
puts("Found resolved versions for #{resolved_versions.size} packages")
|
|
83
|
+
warn_for_empty_xcode_project(remote_packages, resolved_versions, xcodeproj_path)
|
|
84
|
+
|
|
85
|
+
check_packages(remote_packages, resolved_versions)
|
|
86
|
+
@warnings
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Check for SPM updates using one or more `Package.swift` manifests as the
|
|
90
|
+
# source of dependencies.
|
|
91
|
+
#
|
|
92
|
+
# Resolved pins from every `Package.resolved` are merged by normalized
|
|
93
|
+
# repository URL into a single lookup. Each manifest's direct dependencies are
|
|
94
|
+
# then compared against that lookup, and the originating manifest is attached
|
|
95
|
+
# to every warning so multi-manifest repos can tell where an update applies.
|
|
96
|
+
#
|
|
97
|
+
# @param [Array<String>] manifest_paths Paths to one or more `Package.swift`
|
|
98
|
+
# @param [Array<String>, nil] resolved_paths Optional explicit
|
|
99
|
+
# `Package.resolved` paths. When omitted, a `Package.resolved` next to
|
|
100
|
+
# each manifest is used.
|
|
101
|
+
# @raise [ManifestParser::CouldNotFindResolvedFile] if no resolved file exists
|
|
102
|
+
# @return [Array<String>] Array of warning messages
|
|
103
|
+
def check_manifests(manifest_paths, resolved_paths = nil)
|
|
104
|
+
clear_warnings
|
|
105
|
+
reset_version_tags_cache
|
|
106
|
+
normalize_ignore_repos
|
|
107
|
+
normalize_allow_hosts
|
|
108
|
+
|
|
109
|
+
resolved_versions = merged_resolved_versions(manifest_paths, resolved_paths)
|
|
110
|
+
puts("Found resolved versions for #{resolved_versions.size} packages")
|
|
111
|
+
|
|
112
|
+
manifest_paths.each { |manifest_path|
|
|
113
|
+
remote_packages = ManifestParser.get_packages(manifest_path)
|
|
114
|
+
check_packages(remote_packages, resolved_versions, manifest_path)
|
|
115
|
+
}
|
|
116
|
+
@warnings
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
def normalize_ignore_repos
|
|
122
|
+
@ignore_repos = Array(@ignore_repos).map { |repo| GitOperations.trim_repo_url(repo) }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def normalize_allow_hosts
|
|
126
|
+
raw_allow_hosts = configured_allow_hosts
|
|
127
|
+
@allow_hosts = raw_allow_hosts.filter_map { |host| AllowHostNormalizer.normalize(host) }
|
|
128
|
+
return unless invalid_allow_hosts_configuration?(raw_allow_hosts)
|
|
129
|
+
|
|
130
|
+
raise(ArgumentError, "allow-hosts was configured, but no entries could be parsed as hostnames")
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def configured_allow_hosts
|
|
134
|
+
AllowHostNormalizer.configured_entries(@allow_hosts)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def invalid_allow_hosts_configuration?(raw_allow_hosts)
|
|
138
|
+
raw_allow_hosts.any? && @allow_hosts.empty?
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def clear_warnings
|
|
142
|
+
@warnings.clear
|
|
143
|
+
@warning_details.clear
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def warn_for_empty_xcode_project(remote_packages, resolved_versions, xcodeproj_path)
|
|
147
|
+
return unless remote_packages.empty? && !resolved_versions.empty?
|
|
148
|
+
|
|
149
|
+
puts(
|
|
150
|
+
"WARNING: No XCRemoteSwiftPackageReference entries were found in #{xcodeproj_path}, " \
|
|
151
|
+
"but Package.resolved contains resolved packages. If dependencies are declared in " \
|
|
152
|
+
"Package.swift files, use package-manifest-paths instead."
|
|
153
|
+
)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def reset_version_tags_cache
|
|
157
|
+
@version_tags_cache = {}
|
|
158
|
+
@version_tag_lookup_errors = {}
|
|
159
|
+
@reported_lookup_failures = {}
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Merge the resolved pins of every relevant `Package.resolved` file.
|
|
163
|
+
#
|
|
164
|
+
# Every expected resolved file must exist. A missing one would silently drop a
|
|
165
|
+
# manifest's pins and produce misleading "all up to date" results, so we fail
|
|
166
|
+
# loudly and name the missing file(s) instead.
|
|
167
|
+
#
|
|
168
|
+
# @return [Hash<String, String>]
|
|
169
|
+
def merged_resolved_versions(manifest_paths, resolved_paths)
|
|
170
|
+
paths = Array(resolved_paths).map(&:to_s).reject(&:empty?)
|
|
171
|
+
paths = manifest_paths.map { |manifest| ManifestParser.default_resolved_path(manifest) } if paths.empty?
|
|
172
|
+
|
|
173
|
+
missing = paths.reject { |path| File.exist?(path) }
|
|
174
|
+
raise(ManifestParser::CouldNotFindResolvedFile, missing.join(", ")) unless missing.empty?
|
|
175
|
+
|
|
176
|
+
puts("Reading resolved packages from: #{paths}")
|
|
177
|
+
paths.each_with_object({}) { |path, pins| pins.merge!(resolved_versions_from(path)) }
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def resolved_versions_from(path)
|
|
181
|
+
PackageResolved.versions_from(path)
|
|
182
|
+
rescue PackageResolved::MalformedFileError => error
|
|
183
|
+
raise unless @malformed_resolved_handler
|
|
184
|
+
|
|
185
|
+
@malformed_resolved_handler.call(path, error)
|
|
186
|
+
{}
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Compare a set of declared dependencies against the resolved pins.
|
|
190
|
+
#
|
|
191
|
+
# Packages are keyed by their normalized repository URL, which is what we match
|
|
192
|
+
# against `resolved_versions` and `ignore_repos`. The original, scheme-bearing
|
|
193
|
+
# URL travels in the entry as `repository_url` and is what we hand to git --
|
|
194
|
+
# the normalized key is not a valid git remote.
|
|
195
|
+
#
|
|
196
|
+
# @param remote_packages [Hash<String, Hash>] normalized URL => { "repository_url", "requirement" }
|
|
197
|
+
# @param resolved_versions [Hash<String, String>] normalized URL => version
|
|
198
|
+
# @param source [String, nil] the manifest a warning should be attributed to
|
|
199
|
+
def check_packages(remote_packages, resolved_versions, source = nil)
|
|
200
|
+
packages = package_contexts(remote_packages, resolved_versions, source)
|
|
201
|
+
prefetch_version_tags(packages)
|
|
202
|
+
|
|
203
|
+
packages.each { |package|
|
|
204
|
+
lookup_error = @version_tag_lookup_errors[package.cache_key]
|
|
205
|
+
if lookup_error
|
|
206
|
+
raise(lookup_error) unless @lookup_failure_handler
|
|
207
|
+
|
|
208
|
+
next report_lookup_failure(package, lookup_error)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
check_package_handling_lookup_failure(package)
|
|
212
|
+
}
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def check_package_handling_lookup_failure(package)
|
|
216
|
+
check_package(package)
|
|
217
|
+
rescue GitOperations::LsRemoteError => error
|
|
218
|
+
raise unless @lookup_failure_handler
|
|
219
|
+
|
|
220
|
+
@version_tag_lookup_errors[package.cache_key] = error
|
|
221
|
+
report_lookup_failure(package, error)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# A dependency shared by several manifests fails its lookup once per run:
|
|
225
|
+
# the error is cached so it is never re-fetched, and reported to the handler
|
|
226
|
+
# only the first time it is seen.
|
|
227
|
+
def report_lookup_failure(package, error)
|
|
228
|
+
key = package.cache_key
|
|
229
|
+
return if @reported_lookup_failures.key?(key)
|
|
230
|
+
|
|
231
|
+
@reported_lookup_failures[key] = true
|
|
232
|
+
@lookup_failure_handler.call(package, error)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def package_contexts(remote_packages, resolved_versions, source)
|
|
236
|
+
remote_packages.filter_map { |normalized_url, entry|
|
|
237
|
+
next if @ignore_repos.include?(normalized_url)
|
|
238
|
+
|
|
239
|
+
repository_url = entry["repository_url"]
|
|
240
|
+
requirement = entry["requirement"]
|
|
241
|
+
next unless requirement
|
|
242
|
+
|
|
243
|
+
name = GitOperations.repo_name(normalized_url)
|
|
244
|
+
|
|
245
|
+
resolved_version = resolved_versions[normalized_url]
|
|
246
|
+
|
|
247
|
+
unless resolved_version
|
|
248
|
+
puts("Unable to locate the current version for #{name} (#{self.class.redact_credentials(repository_url)})")
|
|
249
|
+
next
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
kind = requirement["kind"]
|
|
253
|
+
validate_repository_host(name, repository_url, source) if git_lookup_required?(kind)
|
|
254
|
+
|
|
255
|
+
SpmPackageContext.new(
|
|
256
|
+
cache_key: version_tags_cache_key(normalized_url, repository_url),
|
|
257
|
+
kind:,
|
|
258
|
+
name:,
|
|
259
|
+
normalized_url:,
|
|
260
|
+
repository_url:,
|
|
261
|
+
persistent_cache_key: VersionTagsPersistentCache.cache_key(normalized_url, repository_url),
|
|
262
|
+
requirement:,
|
|
263
|
+
resolved_version:,
|
|
264
|
+
source:
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def validate_repository_host(name, repository_url, source)
|
|
270
|
+
return if @allow_hosts.empty?
|
|
271
|
+
|
|
272
|
+
host = GitOperations.host(repository_url)
|
|
273
|
+
return if host && @allow_hosts.include?(host)
|
|
274
|
+
|
|
275
|
+
raise(DisallowedRepositoryHost, disallowed_repository_host_message(name, source, host || "unknown host"))
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def disallowed_repository_host_message(name, source, host_note)
|
|
279
|
+
source_note = source ? " from #{source}" : ""
|
|
280
|
+
"Repository host #{host_note.inspect} for #{name}#{source_note} is not allowed by allow-hosts (allowed: #{@allow_hosts.join(', ')})"
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def version_tags_cache_key(normalized_url, repository_url)
|
|
284
|
+
"#{normalized_url}\n#{repository_url}"
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Failed lookups land in @version_tag_lookup_errors keyed by cache key
|
|
288
|
+
# (always empty when no lookup_failure_handler is configured -- failures
|
|
289
|
+
# raise instead).
|
|
290
|
+
def prefetch_version_tags(packages)
|
|
291
|
+
pending = pending_version_tag_lookups(packages)
|
|
292
|
+
return if pending.empty?
|
|
293
|
+
|
|
294
|
+
persistent_cache = VersionTagsPersistentCache.new(directory: @version_tags_cache_dir, ttl_seconds: @version_tags_cache_ttl_seconds)
|
|
295
|
+
results, errors = VersionTagFetcher.call(
|
|
296
|
+
pending,
|
|
297
|
+
worker_limit: VERSION_TAG_WORKER_COUNT,
|
|
298
|
+
persistent_cache:,
|
|
299
|
+
raise_on_error: !@lookup_failure_handler
|
|
300
|
+
)
|
|
301
|
+
@version_tags_cache.merge!(results)
|
|
302
|
+
@version_tag_lookup_errors.merge!(errors)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def pending_version_tag_lookups(packages)
|
|
306
|
+
packages.each_with_object({}) { |package, lookups|
|
|
307
|
+
next unless version_tag_lookup_required?(package.kind)
|
|
308
|
+
next if @version_tag_lookup_errors.key?(package.cache_key)
|
|
309
|
+
|
|
310
|
+
package.add_version_tag_lookup(lookups, @version_tags_cache)
|
|
311
|
+
}.values
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def version_tag_lookup_required?(kind)
|
|
315
|
+
{
|
|
316
|
+
"branch" => false,
|
|
317
|
+
"revision" => @check_revisions,
|
|
318
|
+
"exactVersion" => @check_when_exact
|
|
319
|
+
}.fetch(kind, true)
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def git_lookup_required?(kind)
|
|
323
|
+
return @check_branches if kind == "branch"
|
|
324
|
+
|
|
325
|
+
version_tag_lookup_required?(kind)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def version_tags_for(package)
|
|
329
|
+
@version_tags_cache.fetch(package.cache_key, [])
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def newest_reportable_version(available_versions)
|
|
333
|
+
available_versions.find { |version| reportable_version?(version) }
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def reportable_version?(version)
|
|
337
|
+
@report_pre_releases || !version.pre
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def check_package(package)
|
|
341
|
+
available_versions = version_tags_for(package)
|
|
342
|
+
|
|
343
|
+
case package.kind
|
|
344
|
+
when "branch"
|
|
345
|
+
warn_for_branch(package) if @check_branches
|
|
346
|
+
when "revision"
|
|
347
|
+
warn_for_revision(package, available_versions) if @check_revisions
|
|
348
|
+
else
|
|
349
|
+
check_versioned_package(package, available_versions)
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def check_versioned_package(package, available_versions = nil)
|
|
354
|
+
kind = package.kind
|
|
355
|
+
repository_url = package.repository_url
|
|
356
|
+
return if kind == "exactVersion" && !@check_when_exact
|
|
357
|
+
|
|
358
|
+
available_versions ||= GitOperations.version_tags(repository_url)
|
|
359
|
+
return if available_versions.empty?
|
|
360
|
+
return if available_versions.first.to_s == package.resolved_version
|
|
361
|
+
|
|
362
|
+
case kind
|
|
363
|
+
when "exactVersion"
|
|
364
|
+
warn_for_new_versions_exact(package, available_versions)
|
|
365
|
+
when "upToNextMajorVersion"
|
|
366
|
+
warn_for_new_versions(package, available_versions, :major)
|
|
367
|
+
when "upToNextMinorVersion"
|
|
368
|
+
warn_for_new_versions(package, available_versions, :minor)
|
|
369
|
+
when "versionRange"
|
|
370
|
+
warn_for_new_versions_range(package, available_versions)
|
|
371
|
+
else
|
|
372
|
+
puts("Not processing dependency rule '#{kind}' for #{package.name} (#{self.class.redact_credentials(repository_url)})")
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def add_warning(message, package, detail)
|
|
377
|
+
record = warning_detail_record(message, package, detail)
|
|
378
|
+
return if @repository_update_rules.suppressed?(record)
|
|
379
|
+
|
|
380
|
+
record_warning(message, package, record)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def record_warning(message, package, record)
|
|
384
|
+
@warnings << [message, package.source_line].compact.join("\n")
|
|
385
|
+
@warning_details << record
|
|
386
|
+
puts("WARNING: #{message}#{package.source_suffix}")
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def warning_detail_record(message, package, detail)
|
|
390
|
+
detail.merge(message:, source: package.source).compact
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def warning_detail(type, package, available_version, note = nil)
|
|
394
|
+
{
|
|
395
|
+
type: type.to_s,
|
|
396
|
+
package: package.name,
|
|
397
|
+
normalized_url: package.normalized_url,
|
|
398
|
+
repository_url: package.repository_url,
|
|
399
|
+
current_version: package.resolved_version.to_s,
|
|
400
|
+
available_version: available_version.to_s,
|
|
401
|
+
note:
|
|
402
|
+
}.merge(UpgradeSuggestion.fields(package, available_version, type))
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# Warns if the branch has a newer commit than the resolved version.
|
|
406
|
+
def warn_for_branch(package)
|
|
407
|
+
warning = package.branch_update_warning
|
|
408
|
+
return unless warning
|
|
409
|
+
|
|
410
|
+
message, last_commit, note = warning
|
|
411
|
+
|
|
412
|
+
add_warning(
|
|
413
|
+
message,
|
|
414
|
+
package,
|
|
415
|
+
warning_detail(:branch, package, last_commit, note)
|
|
416
|
+
)
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
# Reports the latest tagged version for a dependency pinned to a raw revision.
|
|
420
|
+
# There is no general way to know whether an arbitrary commit is behind, so
|
|
421
|
+
# this is purely informational and only runs when +check_revisions+ is enabled.
|
|
422
|
+
def warn_for_revision(package, available_versions)
|
|
423
|
+
newest_version = newest_reportable_version(available_versions)
|
|
424
|
+
return unless newest_version
|
|
425
|
+
|
|
426
|
+
add_warning(
|
|
427
|
+
"#{package.name} is pinned to a revision (#{package.resolved_version}); latest tagged version is #{newest_version}",
|
|
428
|
+
package,
|
|
429
|
+
warning_detail(:revision, package, newest_version, "revision pin")
|
|
430
|
+
)
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def warn_for_new_versions_exact(package, available_versions)
|
|
434
|
+
resolved_version = package.resolved_version
|
|
435
|
+
newest_version = newest_reportable_version(available_versions)
|
|
436
|
+
return unless newest_version
|
|
437
|
+
return if newest_version.to_s == resolved_version
|
|
438
|
+
|
|
439
|
+
add_warning(
|
|
440
|
+
"Newer version of #{package.name}: #{newest_version} (but this package is set to exact version #{resolved_version})",
|
|
441
|
+
package,
|
|
442
|
+
warning_detail(:version, package, newest_version, "exact version")
|
|
443
|
+
)
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def warn_for_new_versions_range(package, available_versions)
|
|
447
|
+
name = package.name
|
|
448
|
+
requirement = package.requirement
|
|
449
|
+
resolved_version = package.resolved_version
|
|
450
|
+
|
|
451
|
+
begin
|
|
452
|
+
max_version = SpmVersionUpdates::Semver.new(requirement["maximumVersion"])
|
|
453
|
+
rescue ArgumentError => error
|
|
454
|
+
puts("Unable to extract semver from #{requirement} for #{name} (#{error})")
|
|
455
|
+
return
|
|
456
|
+
end
|
|
457
|
+
# Honor the pre-release policy: never report a pre-release as the newest
|
|
458
|
+
# version when report_pre_releases is false.
|
|
459
|
+
newest = newest_reportable_version(available_versions)
|
|
460
|
+
return unless newest
|
|
461
|
+
|
|
462
|
+
if newest < max_version
|
|
463
|
+
unless newest.to_s == resolved_version
|
|
464
|
+
add_warning(
|
|
465
|
+
"Newer version of #{name}: #{newest}",
|
|
466
|
+
package,
|
|
467
|
+
warning_detail(:version, package, newest, "version range")
|
|
468
|
+
)
|
|
469
|
+
end
|
|
470
|
+
else
|
|
471
|
+
newest_meeting_reqs = available_versions.find { |version|
|
|
472
|
+
version < max_version && reportable_version?(version)
|
|
473
|
+
}
|
|
474
|
+
unless newest_meeting_reqs.nil? || newest_meeting_reqs.to_s == resolved_version
|
|
475
|
+
add_warning(
|
|
476
|
+
"Newer version of #{name}: #{newest_meeting_reqs}",
|
|
477
|
+
package,
|
|
478
|
+
warning_detail(:version, package, newest_meeting_reqs, "version range")
|
|
479
|
+
)
|
|
480
|
+
end
|
|
481
|
+
if @report_above_maximum
|
|
482
|
+
add_warning(
|
|
483
|
+
"Newest version of #{name}: #{newest} (but this package is configured up to the next #{max_version} version)",
|
|
484
|
+
package,
|
|
485
|
+
warning_detail(:above_maximum, package, newest, "above configured maximum")
|
|
486
|
+
)
|
|
487
|
+
end
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def warn_for_new_versions(package, available_versions, major_or_minor)
|
|
492
|
+
name = package.name
|
|
493
|
+
resolved_version_string = package.resolved_version
|
|
494
|
+
|
|
495
|
+
begin
|
|
496
|
+
resolved_version = SpmVersionUpdates::Semver.new(resolved_version_string)
|
|
497
|
+
rescue ArgumentError => error
|
|
498
|
+
puts("Unable to extract semver from #{resolved_version_string} for #{name} (#{error})")
|
|
499
|
+
return
|
|
500
|
+
end
|
|
501
|
+
# upToNextMajor allows any version with the same major; upToNextMinor additionally
|
|
502
|
+
# requires the same minor. Comparing minor alone would wrongly match e.g. 2.5.0
|
|
503
|
+
# against a resolved 1.5.0.
|
|
504
|
+
newest_meeting_reqs = available_versions.find { |version|
|
|
505
|
+
version.major == resolved_version.major &&
|
|
506
|
+
(major_or_minor == :major || version.minor == resolved_version.minor) &&
|
|
507
|
+
reportable_version?(version)
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
unless newest_meeting_reqs.nil? || newest_meeting_reqs == resolved_version
|
|
511
|
+
add_warning(
|
|
512
|
+
"Newer version of #{name}: #{newest_meeting_reqs}",
|
|
513
|
+
package,
|
|
514
|
+
warning_detail(:version, package, newest_meeting_reqs, "up to next #{major_or_minor}")
|
|
515
|
+
)
|
|
516
|
+
end
|
|
517
|
+
return unless @report_above_maximum
|
|
518
|
+
|
|
519
|
+
newest_above_reqs = newest_reportable_version(available_versions)
|
|
520
|
+
# Suppressed only when nothing exists above the constraint (the newest overall
|
|
521
|
+
# is the newest in-constraint version). Being at the newest in-constraint
|
|
522
|
+
# version is intentionally still reported here, since report_above_maximum
|
|
523
|
+
# exists precisely to surface the out-of-range (e.g. next major) version.
|
|
524
|
+
return if newest_above_reqs == newest_meeting_reqs
|
|
525
|
+
|
|
526
|
+
add_warning(
|
|
527
|
+
"Newest version of #{name}: #{newest_above_reqs} (but this package is configured up to the next #{major_or_minor} version)",
|
|
528
|
+
package,
|
|
529
|
+
warning_detail(:above_maximum, package, newest_above_reqs, "above configured maximum")
|
|
530
|
+
)
|
|
531
|
+
end
|
|
532
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "git_operations"
|
|
4
|
+
|
|
5
|
+
# Carries normalized package facts through version, branch, and revision checks.
|
|
6
|
+
SpmPackageContext = Struct.new(
|
|
7
|
+
:cache_key,
|
|
8
|
+
:kind,
|
|
9
|
+
:name,
|
|
10
|
+
:normalized_url,
|
|
11
|
+
:repository_url,
|
|
12
|
+
:persistent_cache_key,
|
|
13
|
+
:requirement,
|
|
14
|
+
:resolved_version,
|
|
15
|
+
:source,
|
|
16
|
+
keyword_init: true
|
|
17
|
+
) {
|
|
18
|
+
def add_version_tag_lookup(lookups, cache)
|
|
19
|
+
key = cache_key
|
|
20
|
+
lookups[key] ||= [cache_key, repository_url, persistent_cache_key] unless cache.key?(key)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def branch
|
|
24
|
+
requirement["branch"]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def branch_update?(last_commit)
|
|
28
|
+
last_commit && last_commit != resolved_version
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def branch_warning_message(last_commit)
|
|
32
|
+
"Newer commit available for #{name} (#{branch}): #{last_commit}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def branch_update_warning
|
|
36
|
+
last_commit = GitOperations.branch_last_commit(repository_url, branch)
|
|
37
|
+
return unless branch_update?(last_commit)
|
|
38
|
+
|
|
39
|
+
[branch_warning_message(last_commit), last_commit, "branch: #{branch}"]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def source_line
|
|
43
|
+
"Source: #{source}" if source
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def source_suffix
|
|
47
|
+
source ? " (#{source})" : ""
|
|
48
|
+
end
|
|
49
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "semver"
|
|
4
|
+
|
|
5
|
+
# Classifies semantic-version update deltas for reporting and fail thresholds.
|
|
6
|
+
module UpdateSeverity
|
|
7
|
+
LEVELS = ["major", "minor", "patch"].freeze
|
|
8
|
+
SEVERITY_RECORD_TYPES = ["", "version", "above_maximum"].freeze
|
|
9
|
+
THRESHOLD_LEVELS = {
|
|
10
|
+
"major" => ["major"],
|
|
11
|
+
"minor" => ["major", "minor"],
|
|
12
|
+
"patch" => ["major", "minor", "patch"]
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
def self.apply(record)
|
|
16
|
+
return record unless SEVERITY_RECORD_TYPES.include?(record["type"].to_s)
|
|
17
|
+
|
|
18
|
+
severity = for_versions(record["current_version"], record["available_version"])
|
|
19
|
+
severity ? record.merge("severity" => severity) : record
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.for_versions(current_version, available_version)
|
|
23
|
+
versions = parse_versions(current_version, available_version)
|
|
24
|
+
return nil unless versions
|
|
25
|
+
|
|
26
|
+
current, available = versions
|
|
27
|
+
available > current ? numeric_delta(current, available) : nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.counts(records)
|
|
31
|
+
records.each_with_object(zero_counts) { |record, result|
|
|
32
|
+
severity = record["severity"]
|
|
33
|
+
result[severity] += 1 if result.key?(severity)
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.count_at_or_above(counts, threshold)
|
|
38
|
+
Array(THRESHOLD_LEVELS[threshold]).sum { |severity| counts.fetch(severity, 0) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.threshold?(value)
|
|
42
|
+
THRESHOLD_LEVELS.key?(value)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.zero_counts
|
|
46
|
+
LEVELS.to_h { |severity| [severity, 0] }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.parse_version(value)
|
|
50
|
+
SpmVersionUpdates::Semver.new(value.to_s)
|
|
51
|
+
rescue ArgumentError
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.parse_versions(current_version, available_version)
|
|
56
|
+
current = parse_version(current_version)
|
|
57
|
+
available = parse_version(available_version)
|
|
58
|
+
current && available ? [current, available] : nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.numeric_delta(current, available)
|
|
62
|
+
return "major" unless available.major == current.major
|
|
63
|
+
return "minor" unless available.minor == current.minor
|
|
64
|
+
|
|
65
|
+
"patch"
|
|
66
|
+
end
|
|
67
|
+
end
|