still_active 3.0.0.rc4 → 3.0.0.rc5

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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +3 -0
  3. data/lib/helpers/activity_helper.rb +3 -3
  4. data/lib/helpers/alternatives_helper.rb +1 -1
  5. data/lib/helpers/ansi_helper.rb +1 -1
  6. data/lib/helpers/bot_context.rb +7 -7
  7. data/lib/helpers/bundler_helper.rb +1 -2
  8. data/lib/helpers/catalog_index.rb +1 -1
  9. data/lib/helpers/constraint_helper.rb +6 -6
  10. data/lib/helpers/cvss_helper.rb +2 -2
  11. data/lib/helpers/cyclonedx_helper.rb +14 -14
  12. data/lib/helpers/diff_markdown_helper.rb +4 -4
  13. data/lib/helpers/dotnet_helper.rb +4 -4
  14. data/lib/helpers/endoflife_helper.rb +3 -3
  15. data/lib/helpers/http_helper.rb +31 -19
  16. data/lib/helpers/lockfile_dependency_parser.rb +2 -2
  17. data/lib/helpers/markdown_escape.rb +1 -1
  18. data/lib/helpers/markdown_helper.rb +7 -7
  19. data/lib/helpers/ruby_advisory_db.rb +1 -1
  20. data/lib/helpers/ruby_helper.rb +3 -3
  21. data/lib/helpers/runtime_ceiling_helper.rb +2 -2
  22. data/lib/helpers/sarif_helper.rb +34 -34
  23. data/lib/helpers/status_helper.rb +1 -1
  24. data/lib/helpers/summary_helper.rb +1 -1
  25. data/lib/helpers/terminal_helper.rb +8 -8
  26. data/lib/helpers/vulnerability_helper.rb +1 -1
  27. data/lib/still_active/artifactory_client.rb +47 -18
  28. data/lib/still_active/cli.rb +38 -38
  29. data/lib/still_active/compact_index_client.rb +117 -0
  30. data/lib/still_active/config.rb +1 -1
  31. data/lib/still_active/config_file.rb +4 -4
  32. data/lib/still_active/deps_dev_client.rb +7 -7
  33. data/lib/still_active/diff.rb +12 -12
  34. data/lib/still_active/ecosystem_lens.rb +5 -5
  35. data/lib/still_active/ecosystems_client.rb +7 -7
  36. data/lib/still_active/forgejo_client.rb +3 -3
  37. data/lib/still_active/github_client.rb +6 -6
  38. data/lib/still_active/gitlab_client.rb +3 -3
  39. data/lib/still_active/options.rb +4 -4
  40. data/lib/still_active/osv_client.rb +8 -8
  41. data/lib/still_active/poison_security_correlator.rb +3 -3
  42. data/lib/still_active/pypi_client.rb +2 -2
  43. data/lib/still_active/repository.rb +4 -4
  44. data/lib/still_active/sarif/rules.rb +20 -20
  45. data/lib/still_active/sbom_reader.rb +8 -8
  46. data/lib/still_active/sbom_workflow.rb +7 -7
  47. data/lib/still_active/source_credentials.rb +39 -0
  48. data/lib/still_active/version.rb +1 -1
  49. data/lib/still_active/workflow.rb +57 -31
  50. data/still_active.gemspec +16 -20
  51. metadata +5 -115
@@ -38,8 +38,8 @@ module StillActive
38
38
  latest_release_date: latest_release_date,
39
39
  libyear: LibyearHelper.gem_libyear(
40
40
  version_used_release_date: current_release_date,
41
- latest_version_release_date: latest_release_date,
42
- ),
41
+ latest_version_release_date: latest_release_date
42
+ )
43
43
  }
44
44
  end
45
45
 
@@ -54,7 +54,7 @@ module StillActive
54
54
  private
55
55
 
56
56
  def current_ruby_version
57
- lockfile_ruby_version || (RUBY_ENGINE == "ruby" ? RUBY_VERSION : nil)
57
+ lockfile_ruby_version || ((RUBY_ENGINE == "ruby") ? RUBY_VERSION : nil)
58
58
  end
59
59
 
60
60
  def lockfile_ruby_version
@@ -82,7 +82,7 @@ module StillActive
82
82
  ceiling_version: ceiling[:version].to_s,
83
83
  ceiling_eol_date: ceiling[:eol_date],
84
84
  oldest_supported: support_window[:oldest_supported].to_s,
85
- latest_stable: support_window[:latest_stable].to_s,
85
+ latest_stable: support_window[:latest_stable].to_s
86
86
  }
87
87
  finding.merge(severity: ConstraintHelper.constraint_severity(finding))
88
88
  end
@@ -92,7 +92,7 @@ module StillActive
92
92
  requirement: requirement,
93
93
  eol_forced: false,
94
94
  oldest_supported: support_window[:oldest_supported].to_s,
95
- latest_stable: support_window[:latest_stable].to_s,
95
+ latest_stable: support_window[:latest_stable].to_s
96
96
  }
97
97
  finding.merge(severity: ConstraintHelper.constraint_severity(finding))
98
98
  end
@@ -40,7 +40,7 @@ module StillActive
40
40
  ruby_info: ruby_info,
41
41
  line_index: line_index,
42
42
  ruby_line: ruby_line,
43
- lockfile_uri: lockfile_uri,
43
+ lockfile_uri: lockfile_uri
44
44
  )
45
45
 
46
46
  JSON.pretty_generate(document(results: results, tool_version: tool_version, flavour: :ruby))
@@ -64,7 +64,7 @@ module StillActive
64
64
  ruby_info: nil,
65
65
  line_index: {},
66
66
  ruby_line: nil,
67
- lockfile_uri: source_uri,
67
+ lockfile_uri: source_uri
68
68
  )
69
69
 
70
70
  JSON.pretty_generate(document(results: results, tool_version: tool_version, flavour: :neutral))
@@ -95,13 +95,13 @@ module StillActive
95
95
  "version" => tool_version,
96
96
  "semanticVersion" => VersionHelper.to_semver(tool_version),
97
97
  "informationUri" => TOOL_URI,
98
- "rules" => catalog.map { |r| sarif_rule(r) },
99
- },
98
+ "rules" => catalog.map { |r| sarif_rule(r) }
99
+ }
100
100
  },
101
- "originalUriBaseIds" => { "%SRCROOT%" => { "uri" => "file:///" } },
101
+ "originalUriBaseIds" => {"%SRCROOT%" => {"uri" => "file:///"}},
102
102
  "results" => results.map { |res| stamp_rule_index(res, index_by_id) },
103
- "columnKind" => "utf16CodeUnits",
104
- }],
103
+ "columnKind" => "utf16CodeUnits"
104
+ }]
105
105
  }
106
106
  end
107
107
 
@@ -113,17 +113,17 @@ module StillActive
113
113
  end
114
114
 
115
115
  def sarif_rule(r)
116
- properties = { "tags" => r[:tags], "precision" => "high" }
116
+ properties = {"tags" => r[:tags], "precision" => "high"}
117
117
  properties["security-severity"] = r[:security_severity] if r[:security_severity]
118
118
  {
119
119
  "id" => r[:id],
120
120
  "name" => r[:name],
121
- "shortDescription" => { "text" => r[:short] },
122
- "fullDescription" => { "text" => r[:full] },
123
- "help" => { "text" => r[:help_text], "markdown" => r[:help_markdown] },
121
+ "shortDescription" => {"text" => r[:short]},
122
+ "fullDescription" => {"text" => r[:full]},
123
+ "help" => {"text" => r[:help_text], "markdown" => r[:help_markdown]},
124
124
  "helpUri" => Sarif::Rules.help_uri(r[:id]),
125
- "defaultConfiguration" => { "level" => r[:level] },
126
- "properties" => properties,
125
+ "defaultConfiguration" => {"level" => r[:level]},
126
+ "properties" => properties
127
127
  }
128
128
  end
129
129
 
@@ -157,16 +157,16 @@ module StillActive
157
157
  if ActivityHelper.activity_level(data) == :critical
158
158
  activity = ActivityHelper.last_activity(data)
159
159
  years = ((Time.now - activity[:date]) / SECONDS_PER_YEAR).round(1)
160
- noun = activity[:kind] == :release ? "no release" : "no commits"
160
+ noun = (activity[:kind] == :release) ? "no release" : "no commits"
161
161
  out << mark_suppressed(
162
162
  result(
163
163
  "SA002",
164
164
  name,
165
165
  "#{display} #{version}: #{noun} in #{years} years (last #{activity[:date].utc.strftime("%Y-%m-%d")})#{alternatives_suffix(data)}#{transitive_suffix(data)}.",
166
- location,
166
+ location
167
167
  ),
168
168
  display,
169
- :activity,
169
+ :activity
170
170
  )
171
171
  end
172
172
  end
@@ -220,7 +220,7 @@ module StillActive
220
220
  end
221
221
 
222
222
  def language_ceiling_level(data)
223
- { critical: "error", warning: "warning", note: "note" }.fetch(data[:language_ceiling][:severity], "note")
223
+ {critical: "error", warning: "warning", note: "note"}.fetch(data[:language_ceiling][:severity], "note")
224
224
  end
225
225
 
226
226
  def language_ceiling_message(display, version, data)
@@ -234,7 +234,7 @@ module StillActive
234
234
  else
235
235
  "no #{runtime} #{ceiling[:latest_stable]} support yet"
236
236
  end
237
- fix = ceiling[:fixed_by_upgrade] && data[:latest_version] ? "; upgrade to #{data[:latest_version]} to lift it" : ""
237
+ fix = (ceiling[:fixed_by_upgrade] && data[:latest_version]) ? "; upgrade to #{data[:latest_version]} to lift it" : ""
238
238
  "#{display} #{version}: requires #{runtime} #{ceiling[:requirement]}, #{body}#{fix}#{transitive_suffix(data)}."
239
239
  end
240
240
 
@@ -250,7 +250,7 @@ module StillActive
250
250
  # security finding, not the maintenance-tier signal poison usually is.
251
251
  return "error" if data[:poison_security_relevant]
252
252
 
253
- { critical: "error", warning: "warning", note: "note" }.fetch(data[:poison_severity], "warning")
253
+ {critical: "error", warning: "warning", note: "note"}.fetch(data[:poison_severity], "warning")
254
254
  end
255
255
 
256
256
  def poison_message(display, version, data)
@@ -290,14 +290,14 @@ module StillActive
290
290
  def mark_suppressed(result_hash, gem_name, signal, advisory: nil, aliases: [])
291
291
  config = StillActive.config
292
292
  if config.ignored_gems.include?(gem_name)
293
- result_hash["suppressions"] = [{ "kind" => "external", "justification" => "ignored via --ignore" }]
293
+ result_hash["suppressions"] = [{"kind" => "external", "justification" => "ignored via --ignore"}]
294
294
  return result_hash
295
295
  end
296
296
 
297
297
  entry = config.suppressions.match(gem: gem_name, signal: signal, advisory: advisory, aliases: aliases)
298
298
  return result_hash unless entry
299
299
 
300
- suppression = { "kind" => "external" }
300
+ suppression = {"kind" => "external"}
301
301
  suppression["justification"] = entry.reason if entry.reason
302
302
  result_hash["suppressions"] = [suppression]
303
303
  result_hash
@@ -331,9 +331,9 @@ module StillActive
331
331
  "#{display} #{version}: #{advisory_id}#{title}#{alias_suffix}#{no_fix}#{transitive_suffix(data)}.",
332
332
  location,
333
333
  level: level,
334
- fp_extra: advisory_id,
334
+ fp_extra: advisory_id
335
335
  )
336
- base["properties"] = { "security-severity" => severity } if severity
336
+ base["properties"] = {"security-severity" => severity} if severity
337
337
  mark_suppressed(base, display, :vulnerability, advisory: vuln[:id], aliases: Array(vuln[:aliases]))
338
338
  end
339
339
 
@@ -354,13 +354,13 @@ module StillActive
354
354
  base = {
355
355
  "ruleId" => "SA006",
356
356
  "level" => "error",
357
- "message" => { "text" => "Ruby #{version} has reached end-of-life#{eol_part}.#{latest_part}" },
357
+ "message" => {"text" => "Ruby #{version} has reached end-of-life#{eol_part}.#{latest_part}"},
358
358
  "locations" => [{
359
359
  "physicalLocation" => {
360
- "artifactLocation" => { "uri" => lockfile_uri, "uriBaseId" => "%SRCROOT%" },
361
- "region" => { "startLine" => ruby_line },
362
- },
363
- }],
360
+ "artifactLocation" => {"uri" => lockfile_uri, "uriBaseId" => "%SRCROOT%"},
361
+ "region" => {"startLine" => ruby_line}
362
+ }
363
+ }]
364
364
  }
365
365
  apply_fingerprint(base, fingerprint("SA006", "ruby"))
366
366
  end
@@ -370,8 +370,8 @@ module StillActive
370
370
  base = {
371
371
  "ruleId" => rule_id,
372
372
  "level" => level,
373
- "message" => { "text" => message },
374
- "locations" => [location],
373
+ "message" => {"text" => message},
374
+ "locations" => [location]
375
375
  }
376
376
  apply_fingerprint(base, fingerprint(rule_id, gem_name, fp_extra))
377
377
  end
@@ -379,7 +379,7 @@ module StillActive
379
379
  def apply_fingerprint(result, fp)
380
380
  result["partialFingerprints"] = {
381
381
  "primaryLocationLineHash" => fp,
382
- "stillActiveFinding/v1" => fp,
382
+ "stillActiveFinding/v1" => fp
383
383
  }
384
384
  result
385
385
  end
@@ -387,9 +387,9 @@ module StillActive
387
387
  def location_for(gem_name, line_index, lockfile_uri)
388
388
  {
389
389
  "physicalLocation" => {
390
- "artifactLocation" => { "uri" => lockfile_uri, "uriBaseId" => "%SRCROOT%" },
391
- "region" => { "startLine" => line_index[gem_name] || 1 },
392
- },
390
+ "artifactLocation" => {"uri" => lockfile_uri, "uriBaseId" => "%SRCROOT%"},
391
+ "region" => {"startLine" => line_index[gem_name] || 1}
392
+ }
393
393
  }
394
394
  end
395
395
 
@@ -45,7 +45,7 @@ module StillActive
45
45
  # archived != EOL: a repo archived while the gem still publishes recent
46
46
  # releases (development moved to a monorepo) isn't dead -- let the
47
47
  # releases speak, but keep :stale so the archived repo stays a yellow flag.
48
- ActivityHelper.release_recency_level(gem_data) == :ok ? :stale : :archived
48
+ (ActivityHelper.release_recency_level(gem_data) == :ok) ? :stale : :archived
49
49
  when :critical then :legacy # long-dormant but clean: feature-complete, not a fire
50
50
  else level # :stale / :ok / :unknown
51
51
  end
@@ -43,7 +43,7 @@ module StillActive
43
43
  vulnerabilities: vulnerabilities,
44
44
  # The single worst per-gem verdict (plus EOL Ruby), so a consumer reads
45
45
  # one project-level posture without scanning every gem's status.
46
- status: StatusHelper.project_status(result, ruby_info: ruby_info),
46
+ status: StatusHelper.project_status(result, ruby_info: ruby_info)
47
47
  }
48
48
  summary[:ruby_eol] = ruby_info[:eol] == true if ruby_info
49
49
  summary
@@ -39,7 +39,7 @@ module StillActive
39
39
  # "gems" for a native Ruby audit -- calling npm/cargo/go packages "gems" is a
40
40
  # Ruby-ism that reads wrong cross-ecosystem.
41
41
  def dependency_noun(result)
42
- result.each_value.any? { |data| data[:ecosystem] } ? "dependencies" : "gems"
42
+ (result.each_value.any? { |data| data[:ecosystem] }) ? "dependencies" : "gems"
43
43
  end
44
44
 
45
45
  def build_row(name, data)
@@ -49,7 +49,7 @@ module StillActive
49
49
  format_activity(data),
50
50
  format_scorecard(data[:scorecard_score]),
51
51
  format_vulns(data),
52
- format_license(data[:license]),
52
+ format_license(data[:license])
53
53
  ]
54
54
  end
55
55
 
@@ -153,9 +153,9 @@ module StillActive
153
153
  # gems point at the parent (#60), direct ones offer alternatives.
154
154
  def sub_lines(data)
155
155
  lines = if data[:poison]
156
- [poison_line(data), (data[:direct] == false ? nil : alternatives_line(data))]
156
+ [poison_line(data), ((data[:direct] == false) ? nil : alternatives_line(data))]
157
157
  else
158
- [data[:direct] == false ? dependency_path_line(data) : alternatives_line(data)]
158
+ [(data[:direct] == false) ? dependency_path_line(data) : alternatives_line(data)]
159
159
  end
160
160
  # A language-runtime ceiling is orthogonal to poison/alternatives (a gem can
161
161
  # be maintained yet still cap your Ruby), so it always gets its own line.
@@ -206,7 +206,7 @@ module StillActive
206
206
  return if constraints.nil? || constraints.empty?
207
207
 
208
208
  path = data[:dependency_path]
209
- via = data[:direct] == false && path && path.length >= 2 ? " (via #{path.first})" : ""
209
+ via = (data[:direct] == false && path && path.length >= 2) ? " (via #{path.first})" : ""
210
210
  # Colour carries the tier: red = act-now (3+ majors behind), yellow = plan,
211
211
  # dim = a minor/FYI cap (1 behind). A security-relevant cap (it pins a
212
212
  # vulnerable dependency below the fix) is always red -- that's the finding to
@@ -234,7 +234,7 @@ module StillActive
234
234
  end
235
235
 
236
236
  def poison_colour(severity)
237
- { critical: :red, warning: :yellow, note: :dim }.fetch(severity, :yellow)
237
+ {critical: :red, warning: :yellow, note: :dim}.fetch(severity, :yellow)
238
238
  end
239
239
 
240
240
  # A worst-first "N label (X critical, Y note)" summary fragment, coloured by
@@ -339,11 +339,11 @@ module StillActive
339
339
  parts << activity
340
340
  parts << "#{summary[:vulnerabilities]} vulnerabilities"
341
341
  poison_tiers = result.each_value.select { |data| data[:poison] }.map { |data| data[:poison_severity] }
342
- poison_part = tier_summary_part(poison_tiers, "#{poison_tiers.size} poison-#{poison_tiers.size == 1 ? "pill" : "pills"}")
342
+ poison_part = tier_summary_part(poison_tiers, "#{poison_tiers.size} poison-#{(poison_tiers.size == 1) ? "pill" : "pills"}")
343
343
  parts << poison_part if poison_part
344
344
  ceilings = result.each_value.filter_map { |data| data[:language_ceiling] }
345
345
  runtimes = ceilings.map { |ceiling| ceiling[:runtime] }.uniq
346
- runtime_label = runtimes.size == 1 ? runtimes.first : "language"
346
+ runtime_label = (runtimes.size == 1) ? runtimes.first : "language"
347
347
  ceiling_part = tier_summary_part(ceilings.map { |ceiling| ceiling[:severity] }, "#{ceilings.size} #{runtime_label} ceiling#{"s" unless ceilings.size == 1}")
348
348
  parts << ceiling_part if ceiling_part
349
349
  total_libyear = LibyearHelper.total_libyear(result)
@@ -10,7 +10,7 @@ module StillActive
10
10
  # a CVSS-4-only advisory returns cvss3Score 0 (unscored); OSV's
11
11
  # `database_specific.severity` reads correctly for it and rescues a real HIGH from
12
12
  # fail-closed noise. GitHub uses MODERATE where we use medium.
13
- OSV_LABELS = { "critical" => "critical", "high" => "high", "moderate" => "medium", "medium" => "medium", "low" => "low" }.freeze
13
+ OSV_LABELS = {"critical" => "critical", "high" => "high", "moderate" => "medium", "medium" => "medium", "low" => "low"}.freeze
14
14
 
15
15
  def highest_severity(vulnerabilities)
16
16
  return if vulnerabilities.nil? || vulnerabilities.empty?
@@ -5,6 +5,8 @@ require "cgi"
5
5
  require "json"
6
6
  require "uri"
7
7
  require_relative "../helpers/http_helper"
8
+ require_relative "compact_index_client"
9
+ require_relative "source_credentials"
8
10
 
9
11
  module StillActive
10
12
  module ArtifactoryClient
@@ -18,8 +20,18 @@ module StillActive
18
20
  false
19
21
  end
20
22
 
23
+ # Version sources in precedence order:
24
+ # 1. the compact index, the only endpoint that lists what the repo can
25
+ # actually resolve (#142)
26
+ # 2. the versions API, for hosts that serve no compact index
27
+ # 3. AQL, a cache inventory, when neither answers
28
+ # The compact index carries no timestamps, so a compact-index list is dated
29
+ # from the versions API.
21
30
  def versions(gem_name:, source_uri:)
22
31
  headers = auth_headers(gem_name: gem_name, source_uri: source_uri)
32
+ compact = CompactIndexClient.versions(gem_name: gem_name, source_uri: source_uri, headers: headers)
33
+ return dated(compact, gem_name: gem_name, source_uri: source_uri, headers: headers) unless compact.empty?
34
+
23
35
  vs = RubygemsClient.versions(gem_name: gem_name, source_uri: source_uri, headers: headers)
24
36
  return vs unless vs.empty?
25
37
 
@@ -30,14 +42,36 @@ module StillActive
30
42
 
31
43
  private
32
44
 
45
+ # Matched on the exact version number, so the versions API can only annotate a
46
+ # version the compact index already listed. That keeps a name collision from
47
+ # contributing anything: the placeholder gem's versions simply don't match, and
48
+ # the real ones stay undated rather than borrowing a stranger's release date.
49
+ def dated(versions, gem_name:, source_uri:, headers:)
50
+ return versions if versions.all? { |version| version["created_at"] }
51
+
52
+ published = RubygemsClient.versions(gem_name: gem_name, source_uri: source_uri, headers: headers)
53
+ return versions unless published.is_a?(Array)
54
+
55
+ by_number = published.grep(Hash).to_h { |version| [version["number"], version] }
56
+ # compact first: an absent compact-index field must not blank out the value
57
+ # the versions API supplied for the same version.
58
+ versions.map { |version| (by_number[version["number"]] || {}).merge(version.compact) }
59
+ end
60
+
61
+ # Bundler's own per-host credential (bundle config / BUNDLE_<HOST>) wins, exactly
62
+ # as `bundle install` would use it. Only when the host has none do we consider
63
+ # still_active's ambient --artifactory-token, and only for the one host the user
64
+ # explicitly allowlisted: the token is not host-keyed, so sending it to a
65
+ # lockfile-derived host would leak it. See SourceCredentials for the host-keyed
66
+ # store this shares with the generic private-source path.
33
67
  def credentials(gem_name:, source_uri:)
34
- host = URI(source_uri).host
35
- bundler = Bundler.settings[source_uri] || Bundler.settings[host]
68
+ bundler = Bundler.settings.credentials_for(URI(source_uri))
36
69
  return bundler if bundler && !bundler.empty?
37
70
 
38
71
  global = StillActive.config.artifactory_token
39
72
  return unless global
40
73
 
74
+ host = URI(source_uri).host
41
75
  configured_host = StillActive.config.artifactory_host
42
76
  unless configured_host && host&.casecmp?(configured_host)
43
77
  warn_unauthorized_host(gem_name: gem_name, host: host)
@@ -48,23 +82,15 @@ module StillActive
48
82
  end
49
83
 
50
84
  def warn_unauthorized_host(gem_name:, host:)
51
- $stderr.puts(
85
+ warn(
52
86
  "warning: an Artifactory token is set but #{host} (source for #{gem_name}) is not an authorized host, " \
53
87
  "so the token will not be sent. " \
54
- "To allow it, set --artifactory-host=#{host} or STILL_ACTIVE_ARTIFACTORY_HOST=#{host}",
88
+ "To allow it, set --artifactory-host=#{host} or STILL_ACTIVE_ARTIFACTORY_HOST=#{host}"
55
89
  )
56
90
  end
57
91
 
58
92
  def auth_headers(gem_name:, source_uri:)
59
- creds = credentials(gem_name: gem_name, source_uri: source_uri)
60
- return {} unless creds
61
-
62
- if creds.include?(":")
63
- user, pass = creds.split(":", 2).map { |part| CGI.unescape(part) }
64
- { "Authorization" => "Basic #{["#{user}:#{pass}"].pack("m0")}" }
65
- else
66
- { "Authorization" => "Bearer #{creds}" }
67
- end
93
+ SourceCredentials.auth_header(credentials(gem_name: gem_name, source_uri: source_uri))
68
94
  end
69
95
 
70
96
  # Artifactory's Rubygems-compatible API
@@ -99,8 +125,8 @@ module StillActive
99
125
  base = URI(artifactory_base)
100
126
  path = "#{base.path}#{AQL_PATH}"
101
127
  query = {
102
- "name" => { "$match" => "#{gem_name}-*.gem" },
103
- "repo" => repo_key,
128
+ "name" => {"$match" => "#{gem_name}-*.gem"},
129
+ "repo" => repo_key
104
130
  }
105
131
  body = %(items.find(#{JSON.generate(query)}).include("repo", "path", "name", "created"))
106
132
  response = HttpHelper.post_json(base, path, body: body, headers: headers.merge("Content-Type" => "text/plain"))
@@ -115,7 +141,7 @@ module StillActive
115
141
  def parse_source_url(source_uri)
116
142
  match = source_uri.match(SOURCE_URL_PATTERN)
117
143
  unless match
118
- $stderr.puts("warning: unrecognized Artifactory source URL for AQL fallback: #{source_uri}")
144
+ warn("warning: unrecognized Artifactory source URL for AQL fallback: #{source_uri}")
119
145
  return [nil, nil]
120
146
  end
121
147
 
@@ -128,10 +154,13 @@ module StillActive
128
154
  version = extract_version(item["name"], gem_name)
129
155
  next if version.nil?
130
156
 
157
+ # No "created_at": the AQL `created` field is when this Artifactory
158
+ # cached the artifact, not when the version was published. The two
159
+ # agree to within a day for a gem someone pulled promptly and diverge
160
+ # by years for one pulled late, so it cannot back a staleness signal.
131
161
  {
132
162
  "number" => version,
133
- "created_at" => item["created"],
134
- "prerelease" => Gem::Version.new(version).prerelease?,
163
+ "prerelease" => Gem::Version.new(version).prerelease?
135
164
  }
136
165
  end
137
166
  .uniq { |h| h["number"] }