still_active 1.5.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +18 -0
- data/lib/helpers/alternatives_helper.rb +42 -0
- data/lib/helpers/catalog_index.rb +98 -0
- data/lib/helpers/markdown_helper.rb +11 -0
- data/lib/helpers/sarif_helper.rb +9 -2
- data/lib/helpers/terminal_helper.rb +19 -2
- data/lib/still_active/cli.rb +2 -0
- data/lib/still_active/config.rb +3 -1
- data/lib/still_active/options.rb +1 -0
- data/lib/still_active/version.rb +1 -1
- data/lib/still_active/workflow.rb +18 -1
- data/still_active.gemspec +5 -1
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6e5e5e599cdd630ec6e916800c4abd7df4a3c9d37e24122e80d8859accb1c4e3
|
|
4
|
+
data.tar.gz: 0cb0dfd2c761612f665a9d2f4e0ba20f47bf8b082c42281af09b8429335aa2a9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 571797c9863bf6597e7c9474e8cf718019925d37d163002e9d594b137458ca0a6be155dfb7a5069777afe83963fb4dc595026a69c8237ce1aa7d024eb254be99
|
|
7
|
+
data.tar.gz: 4e7ba6a7777973b6fe7f1fa54fc1fad00a06639da18944f0bc89befd6a3e794b1d55eeaca62f33b193bebb54cdb2d6d85c63ae48a73adc722e58a3f194c0aeae
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.6.0] - 2026-06-08
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `--alternatives` surfaces up to three maintained alternative gems for any dependency still_active flags as archived or critically abandoned, drawn from the [rubytoolbox/catalog](https://github.com/rubytoolbox/catalog) category data and ranked by total RubyGems downloads. Presented as **leads to verify, not vetted recommendations** — same Ruby Toolbox category does not guarantee a drop-in replacement — reflecting that Ruby has no authoritative successor metadata the way npm (`deprecate`), Go (`// Deprecated:`), or NuGet (alternate-package) do. Opt-in and best-effort: the catalog is fetched once and cached under `XDG_CACHE_HOME` with a 7-day TTL, any fetch/parse failure degrades to silence, and nothing here can block a run or affect `--fail-if-*` exit codes. Leads render in terminal (a dimmed sub-line), markdown (an Alternatives section), JSON (an additive `alternatives` array), and SARIF (appended to the SA001/SA002 result messages); CycloneDX is unchanged. With the flag off, terminal output shows a one-line discoverability hint on flagged gems. Silent when the catalog has no entry for the gem (the common case for niche/long-tail gems). Closes #28.
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- The `async` runtime dependency now requires `>= 2.2` (previously unconstrained). 2.2.0 is the verified real minimum — earlier 2.x releases hit a fiber-scheduler `io_read` bug under still_active's concurrent fan-out. A new CI job installs every runtime dependency at its declared gemspec floor and runs the suite on the minimum supported Ruby, so an under-set floor now fails loudly instead of silently.
|
|
12
|
+
|
|
3
13
|
## [1.5.0] - 2026-05-23
|
|
4
14
|
|
|
5
15
|
### Added
|
data/README.md
CHANGED
|
@@ -53,6 +53,8 @@ The bolded rows are the gap `still_active` fills: nobody else answers "is the ma
|
|
|
53
53
|
gem install still_active
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
+
**Requires an actively-maintained Ruby.** The gemspec's `required_ruby_version` floor tracks Ruby's [EOL schedule](https://endoflife.date/ruby); running a maintenance auditor on an unmaintained runtime would be a bit rich. You don't have to run it *on* the Ruby you're auditing, though: still_active reports on the version your project pins in `Gemfile.lock`, so run it from any current Ruby (locally, in CI, or via the [`still_active-action`](https://github.com/SeanLF/still_active-action)) and it will still flag an EOL target.
|
|
57
|
+
|
|
56
58
|
## Quick Start
|
|
57
59
|
|
|
58
60
|
```bash
|
|
@@ -99,6 +101,7 @@ Usage: still_active [options]
|
|
|
99
101
|
--terminal Coloured terminal output (default in TTY)
|
|
100
102
|
--markdown Markdown table output
|
|
101
103
|
--json JSON output (default when piped)
|
|
104
|
+
--alternatives Suggest maintained alternatives (Ruby Toolbox leads) for archived/critical gems
|
|
102
105
|
--sarif[=PATH] SARIF 2.1.0 output for GitHub Code Scanning
|
|
103
106
|
--cyclonedx[=PATH] CycloneDX SBOM output (stdout, or a file path)
|
|
104
107
|
--cyclonedx-version=VERSION CycloneDX spec version: 1.6 (default) or 1.7
|
|
@@ -339,6 +342,20 @@ Activity is determined by the most recent signal across last commit date, latest
|
|
|
339
342
|
- **stale**: last activity between 1 and 3 years ago (configurable with `--warning-range-end`)
|
|
340
343
|
- **critical**: last activity over 3 years ago
|
|
341
344
|
|
|
345
|
+
### Alternative gem leads (opt-in)
|
|
346
|
+
|
|
347
|
+
When a gem is flagged archived or critical, `--alternatives` surfaces up to three maintained gems from the same [Ruby Toolbox](https://www.ruby-toolbox.com) category, ranked by total downloads:
|
|
348
|
+
|
|
349
|
+
```bash
|
|
350
|
+
still_active --gems=paperclip --alternatives
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
```text
|
|
354
|
+
↳ leads (Ruby Toolbox): shrine · carrierwave · kt-paperclip (verify fit)
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
These are **leads, not recommendations**: same-category does not mean drop-in replacement, so verify fit before switching. Ruby has no authoritative "use instead" metadata (unlike npm `deprecate`, Go's `// Deprecated:`, or NuGet's alternate-package field), so this is a best-effort heuristic. It is silent when the catalog has no entry for the gem, and the feature never blocks or fails a run. Leads appear in terminal, markdown, JSON, and SARIF output. When the flag is off, terminal output shows a one-line hint on flagged gems that the option exists (other formats stay silent).
|
|
358
|
+
|
|
342
359
|
### Data sources
|
|
343
360
|
|
|
344
361
|
- **Versions, release dates, and licenses** from [RubyGems.org](https://rubygems.org) or [GitHub Packages](https://docs.github.com/en/packages)
|
|
@@ -346,6 +363,7 @@ Activity is determined by the most recent signal across last commit date, latest
|
|
|
346
363
|
- **OpenSSF Scorecard**, **vulnerability counts**, and **CVSS severity** from Google's [deps.dev](https://deps.dev) API
|
|
347
364
|
- **Additional advisories** from [ruby-advisory-db](https://github.com/rubysec/ruby-advisory-db), merged in when `bundler-audit` is installed alongside (run `bundle audit update` to keep its checkout current)
|
|
348
365
|
- **Ruby version freshness** from [endoflife.date](https://endoflife.date)
|
|
366
|
+
- **Alternative gem leads** (with `--alternatives`) from the [rubytoolbox/catalog](https://github.com/rubytoolbox/catalog) category data
|
|
349
367
|
|
|
350
368
|
### Configuration defaults
|
|
351
369
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "gems"
|
|
4
|
+
|
|
5
|
+
module StillActive
|
|
6
|
+
# Turns a gem's catalog siblings into ranked "leads" -- the most-downloaded
|
|
7
|
+
# still-published alternatives. Best-effort: a failed lookup drops that
|
|
8
|
+
# candidate, never the feature.
|
|
9
|
+
module AlternativesHelper
|
|
10
|
+
extend self
|
|
11
|
+
|
|
12
|
+
MAX_SIBLINGS_CONSIDERED = 40 # bound the download lookups for huge categories
|
|
13
|
+
DEFAULT_LIMIT = 3
|
|
14
|
+
|
|
15
|
+
def leads_for(gem_name:, index:, limit: DEFAULT_LIMIT)
|
|
16
|
+
return [] if index.nil?
|
|
17
|
+
|
|
18
|
+
# Bound the per-gem download lookups so a huge category can't trigger
|
|
19
|
+
# dozens of HTTP calls. This is a catalog-order prefix, so a very large
|
|
20
|
+
# category could leave a popular sibling past the cap out of the ranking;
|
|
21
|
+
# acceptable for best-effort leads where we only ever surface a few.
|
|
22
|
+
# (CatalogIndex already reduces owner/repo slugs to their gem-name tail,
|
|
23
|
+
# so every entry here is a plain name rankable by downloads.)
|
|
24
|
+
siblings = (index[gem_name] || []).first(MAX_SIBLINGS_CONSIDERED)
|
|
25
|
+
return [] if siblings.empty?
|
|
26
|
+
|
|
27
|
+
siblings
|
|
28
|
+
.filter_map { |name| (count = downloads(name)) && [name, count] }
|
|
29
|
+
.max_by(limit) { |_name, count| count }
|
|
30
|
+
.map(&:first)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def downloads(gem_name)
|
|
36
|
+
info = Gems.info(gem_name)
|
|
37
|
+
info && info["downloads"]
|
|
38
|
+
rescue StandardError
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
require "zlib"
|
|
5
|
+
require "rubygems/package"
|
|
6
|
+
require "yaml"
|
|
7
|
+
require "json"
|
|
8
|
+
require "open-uri"
|
|
9
|
+
|
|
10
|
+
module StillActive
|
|
11
|
+
# Optional source of "alternative gem" leads: the rubytoolbox/catalog repo
|
|
12
|
+
# (MIT) mapped to gem -> co-category siblings. Fetched once and cached; every
|
|
13
|
+
# path is best-effort, returning nil/empty so a miss just means no leads.
|
|
14
|
+
module CatalogIndex
|
|
15
|
+
extend self
|
|
16
|
+
|
|
17
|
+
REPO = "rubytoolbox/catalog"
|
|
18
|
+
CACHE_TTL_SECONDS = 7 * 24 * 60 * 60
|
|
19
|
+
MAX_DOWNLOAD_BYTES = 25 * 1024 * 1024 # the catalog is ~50 KB; cap to avoid surprises
|
|
20
|
+
|
|
21
|
+
# Returns { gem => [siblings] } or nil. Never raises.
|
|
22
|
+
def load
|
|
23
|
+
cached = read_cache
|
|
24
|
+
return cached if cached
|
|
25
|
+
|
|
26
|
+
blob = download
|
|
27
|
+
index = build_index(blob)
|
|
28
|
+
write_cache(index)
|
|
29
|
+
index
|
|
30
|
+
rescue StandardError => e
|
|
31
|
+
warn("still_active: could not load Ruby Toolbox catalog for alternatives (#{e.class}); skipping leads")
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Parse a gzipped catalog tarball into { gem_name => [sibling gem names] }.
|
|
36
|
+
def build_index(tar_gz_blob)
|
|
37
|
+
categories = []
|
|
38
|
+
|
|
39
|
+
reader = Gem::Package::TarReader.new(Zlib::GzipReader.new(StringIO.new(tar_gz_blob)))
|
|
40
|
+
reader.each do |entry|
|
|
41
|
+
next unless entry.file?
|
|
42
|
+
next unless entry.full_name.match?(%r{/catalog/.+\.ya?ml$})
|
|
43
|
+
next if File.basename(entry.full_name) == "_meta.yml"
|
|
44
|
+
|
|
45
|
+
data = YAML.safe_load(entry.read)
|
|
46
|
+
next unless data.is_a?(Hash) && data["projects"].is_a?(Array)
|
|
47
|
+
|
|
48
|
+
categories << data["projects"].map { |p| p.to_s.split("/").last }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
build_siblings(categories)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def cache_path
|
|
57
|
+
base = ENV["XDG_CACHE_HOME"]
|
|
58
|
+
base = File.join(Dir.home, ".cache") if base.nil? || base.empty?
|
|
59
|
+
File.join(base, "still_active", "catalog-siblings.json")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def read_cache
|
|
63
|
+
path = cache_path
|
|
64
|
+
return unless File.exist?(path)
|
|
65
|
+
return if Time.now - File.mtime(path) > CACHE_TTL_SECONDS
|
|
66
|
+
|
|
67
|
+
JSON.parse(File.read(path))
|
|
68
|
+
rescue JSON::ParserError
|
|
69
|
+
nil
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def write_cache(index)
|
|
73
|
+
path = cache_path
|
|
74
|
+
require "fileutils"
|
|
75
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
76
|
+
File.write(path, JSON.dump(index))
|
|
77
|
+
rescue SystemCallError
|
|
78
|
+
nil # an unwritable cache dir must not break the feature
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def download
|
|
82
|
+
url = StillActive.config.github_client.archive_link(REPO, format: "tarball", ref: "main")
|
|
83
|
+
URI.open(url) { |io| io.read(MAX_DOWNLOAD_BYTES) } # rubocop:disable Security/Open
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def build_siblings(categories)
|
|
87
|
+
siblings = Hash.new { |h, k| h[k] = [] }
|
|
88
|
+
|
|
89
|
+
categories.each do |members|
|
|
90
|
+
members.each do |gem_name|
|
|
91
|
+
siblings[gem_name].concat(members - [gem_name])
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
siblings.transform_values(&:uniq)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -84,6 +84,17 @@ module StillActive
|
|
|
84
84
|
"| #{cells.join(" | ")} |"
|
|
85
85
|
end
|
|
86
86
|
|
|
87
|
+
def alternatives_section(result)
|
|
88
|
+
flagged = result.select do |_name, data|
|
|
89
|
+
data[:alternatives] && !data[:alternatives].empty?
|
|
90
|
+
end
|
|
91
|
+
return "" if flagged.empty?
|
|
92
|
+
|
|
93
|
+
lines = ["", "**Alternatives** (Ruby Toolbox leads, verify fit):"]
|
|
94
|
+
flagged.each { |name, data| lines << "- `#{name}`: #{data[:alternatives].join(", ")}" }
|
|
95
|
+
lines.join("\n")
|
|
96
|
+
end
|
|
97
|
+
|
|
87
98
|
private
|
|
88
99
|
|
|
89
100
|
def version_with_date(text:, url:, date:)
|
data/lib/helpers/sarif_helper.rb
CHANGED
|
@@ -98,7 +98,7 @@ module StillActive
|
|
|
98
98
|
location = location_for(name, line_index, lockfile_uri)
|
|
99
99
|
|
|
100
100
|
if data[:archived]
|
|
101
|
-
out << result("SA001", name, "#{name} #{version}: upstream repository is archived#{repo_suffix(data)}.", location)
|
|
101
|
+
out << result("SA001", name, "#{name} #{version}: upstream repository is archived#{repo_suffix(data)}#{alternatives_suffix(data)}.", location)
|
|
102
102
|
end
|
|
103
103
|
|
|
104
104
|
unless data[:archived]
|
|
@@ -108,7 +108,7 @@ module StillActive
|
|
|
108
108
|
out << result(
|
|
109
109
|
"SA002",
|
|
110
110
|
name,
|
|
111
|
-
"#{name} #{version}: no commits in #{years} years (last #{last_commit.utc.strftime("%Y-%m-%d")}).",
|
|
111
|
+
"#{name} #{version}: no commits in #{years} years (last #{last_commit.utc.strftime("%Y-%m-%d")})#{alternatives_suffix(data)}.",
|
|
112
112
|
location,
|
|
113
113
|
)
|
|
114
114
|
end
|
|
@@ -228,5 +228,12 @@ module StillActive
|
|
|
228
228
|
def repo_suffix(data)
|
|
229
229
|
data[:repository_url] ? " (#{data[:repository_url]})" : ""
|
|
230
230
|
end
|
|
231
|
+
|
|
232
|
+
def alternatives_suffix(data)
|
|
233
|
+
leads = data[:alternatives]
|
|
234
|
+
return "" if leads.nil? || leads.empty?
|
|
235
|
+
|
|
236
|
+
" Consider: #{leads.join(", ")}"
|
|
237
|
+
end
|
|
231
238
|
end
|
|
232
239
|
end
|
|
@@ -13,13 +13,18 @@ module StillActive
|
|
|
13
13
|
HEADERS = ["Name", "Version", "Activity", "OpenSSF", "Vulns", "License"].freeze
|
|
14
14
|
|
|
15
15
|
def render(result, ruby_info: nil)
|
|
16
|
-
|
|
16
|
+
names = result.keys.sort
|
|
17
|
+
rows = names.map { |name| build_row(name, result[name]) }
|
|
17
18
|
widths = column_widths(rows)
|
|
18
19
|
|
|
19
20
|
lines = []
|
|
20
21
|
lines << header_line(widths)
|
|
21
22
|
lines << separator_line(widths)
|
|
22
|
-
|
|
23
|
+
names.each_with_index do |name, i|
|
|
24
|
+
lines << row_line(rows[i], widths)
|
|
25
|
+
extra = alternatives_line(result[name])
|
|
26
|
+
lines << extra if extra
|
|
27
|
+
end
|
|
23
28
|
lines << ""
|
|
24
29
|
lines << summary_line(result)
|
|
25
30
|
lines << ruby_summary_line(ruby_info) if ruby_info
|
|
@@ -125,6 +130,18 @@ module StillActive
|
|
|
125
130
|
.join
|
|
126
131
|
end
|
|
127
132
|
|
|
133
|
+
def alternatives_line(data)
|
|
134
|
+
level = ActivityHelper.activity_level(data)
|
|
135
|
+
return unless [:archived, :critical].include?(level)
|
|
136
|
+
|
|
137
|
+
leads = data[:alternatives]
|
|
138
|
+
if leads && !leads.empty?
|
|
139
|
+
AnsiHelper.dim(" ↳ leads (Ruby Toolbox): #{leads.join(" · ")} (verify fit)")
|
|
140
|
+
elsif !StillActive.config.alternatives
|
|
141
|
+
AnsiHelper.dim(" ↳ run with --alternatives for maintained replacements")
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
128
145
|
def ruby_summary_line(ruby_info)
|
|
129
146
|
version = ruby_info[:version]
|
|
130
147
|
latest = ruby_info[:latest_version]
|
data/lib/still_active/cli.rb
CHANGED
|
@@ -190,6 +190,8 @@ module StillActive
|
|
|
190
190
|
|
|
191
191
|
puts MarkdownHelper.markdown_table_body_line(gem_name: name, data: gem_data)
|
|
192
192
|
end
|
|
193
|
+
alternatives = MarkdownHelper.alternatives_section(result)
|
|
194
|
+
puts alternatives unless alternatives.empty?
|
|
193
195
|
if ruby_info
|
|
194
196
|
puts ""
|
|
195
197
|
puts MarkdownHelper.ruby_line(ruby_info)
|
data/lib/still_active/config.rb
CHANGED
|
@@ -7,7 +7,8 @@ require "open3"
|
|
|
7
7
|
module StillActive
|
|
8
8
|
class Config
|
|
9
9
|
attr_writer :github_oauth_token, :gitlab_token, :gemfile_path
|
|
10
|
-
attr_accessor :
|
|
10
|
+
attr_accessor :alternatives,
|
|
11
|
+
:baseline_path,
|
|
11
12
|
:critical_warning_emoji,
|
|
12
13
|
:cyclonedx_path,
|
|
13
14
|
:cyclonedx_version,
|
|
@@ -28,6 +29,7 @@ module StillActive
|
|
|
28
29
|
:warning_range_end
|
|
29
30
|
|
|
30
31
|
def initialize
|
|
32
|
+
@alternatives = false
|
|
31
33
|
@fail_if_critical = false
|
|
32
34
|
@fail_if_outdated = nil
|
|
33
35
|
@fail_if_vulnerable = nil
|
data/lib/still_active/options.rb
CHANGED
|
@@ -64,6 +64,7 @@ module StillActive
|
|
|
64
64
|
opts.on("--terminal", "Coloured terminal output (default in TTY)") { StillActive.config { |config| config.output_format = :terminal } }
|
|
65
65
|
opts.on("--markdown", "Markdown table output") { StillActive.config { |config| config.output_format = :markdown } }
|
|
66
66
|
opts.on("--json", "JSON output (default when piped)") { StillActive.config { |config| config.output_format = :json } }
|
|
67
|
+
opts.on("--alternatives", "Suggest maintained alternatives (Ruby Toolbox leads) for archived/critical gems") { StillActive.config { |config| config.alternatives = true } }
|
|
67
68
|
opts.on("--sarif[=PATH]", "SARIF 2.1.0 output for GitHub Code Scanning (default path: still_active.sarif.json; '-' for stdout). Overrides --terminal/--markdown/--json.") do |value|
|
|
68
69
|
StillActive.config { |config| config.sarif_path = value || "still_active.sarif.json" }
|
|
69
70
|
end
|
data/lib/still_active/version.rb
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
require_relative "deps_dev_client"
|
|
4
4
|
require_relative "gitlab_client"
|
|
5
5
|
require_relative "repository"
|
|
6
|
+
require_relative "../helpers/activity_helper"
|
|
7
|
+
require_relative "../helpers/alternatives_helper"
|
|
8
|
+
require_relative "../helpers/catalog_index"
|
|
6
9
|
require_relative "../helpers/libyear_helper"
|
|
7
10
|
require_relative "../helpers/ruby_advisory_db"
|
|
8
11
|
require_relative "../helpers/ruby_helper"
|
|
@@ -22,6 +25,7 @@ module StillActive
|
|
|
22
25
|
# Load the optional ruby-advisory-db once, before the fan-out, so the
|
|
23
26
|
# read-only Database is shared across fibers rather than reloaded per gem.
|
|
24
27
|
advisory_db = RubyAdvisoryDb.load
|
|
28
|
+
catalog = StillActive.config.alternatives ? CatalogIndex.load : nil
|
|
25
29
|
barrier = Async::Barrier.new
|
|
26
30
|
semaphore = Async::Semaphore.new(StillActive.config.parallelism, parent: barrier)
|
|
27
31
|
result_object = {}
|
|
@@ -36,6 +40,7 @@ module StillActive
|
|
|
36
40
|
source_type: gem[:source_type] || :rubygems,
|
|
37
41
|
source_uri: gem[:source_uri],
|
|
38
42
|
advisory_db: advisory_db,
|
|
43
|
+
catalog: catalog,
|
|
39
44
|
)
|
|
40
45
|
rescue Octokit::TooManyRequests
|
|
41
46
|
$stderr.print("\r\e[K") if on_progress
|
|
@@ -60,7 +65,7 @@ module StillActive
|
|
|
60
65
|
|
|
61
66
|
private
|
|
62
67
|
|
|
63
|
-
def gem_info(gem_name:, result_object:, gem_version: nil, source_type: :rubygems, source_uri: nil, advisory_db: nil)
|
|
68
|
+
def gem_info(gem_name:, result_object:, gem_version: nil, source_type: :rubygems, source_uri: nil, advisory_db: nil, catalog: nil)
|
|
64
69
|
result_object[gem_name] = { source_type: source_type }
|
|
65
70
|
result_object[gem_name][:version_used] = gem_version if gem_version
|
|
66
71
|
|
|
@@ -76,6 +81,8 @@ module StillActive
|
|
|
76
81
|
advisory_db: advisory_db,
|
|
77
82
|
)
|
|
78
83
|
end
|
|
84
|
+
|
|
85
|
+
attach_alternatives(gem_name: gem_name, result_object: result_object, catalog: catalog)
|
|
79
86
|
end
|
|
80
87
|
|
|
81
88
|
def gem_info_rubygems(gem_name:, gem_version:, result_object:, source_uri:, advisory_db: nil)
|
|
@@ -151,6 +158,16 @@ module StillActive
|
|
|
151
158
|
})
|
|
152
159
|
end
|
|
153
160
|
|
|
161
|
+
def attach_alternatives(gem_name:, result_object:, catalog:)
|
|
162
|
+
return if catalog.nil?
|
|
163
|
+
return unless [:archived, :critical].include?(ActivityHelper.activity_level(result_object[gem_name]))
|
|
164
|
+
|
|
165
|
+
leads = AlternativesHelper.leads_for(gem_name: gem_name, index: catalog)
|
|
166
|
+
result_object[gem_name][:alternatives] = leads unless leads.empty?
|
|
167
|
+
rescue StandardError
|
|
168
|
+
nil # cosmetic best-effort: lead-fetching must never break the core audit
|
|
169
|
+
end
|
|
170
|
+
|
|
154
171
|
def fetch_deps_dev_info(gem_name:, version:, advisory_db: nil)
|
|
155
172
|
info = DepsDevClient.version_info(gem_name: gem_name, version: version)
|
|
156
173
|
scorecard = DepsDevClient.project_scorecard(project_id: info&.dig(:project_id))
|
data/still_active.gemspec
CHANGED
|
@@ -45,7 +45,11 @@ Gem::Specification.new do |spec|
|
|
|
45
45
|
spec.add_development_dependency("rubocop-rspec")
|
|
46
46
|
spec.add_development_dependency("rubocop-shopify")
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
# 2.0/2.1 ship a scheduler that breaks our fan-out (io_read); 2.2 is the
|
|
49
|
+
# verified floor (checked against Ruby 3.3 in Docker). octokit/faraday-retry/
|
|
50
|
+
# gems work down to ancient versions, so they stay unpinned rather than
|
|
51
|
+
# carry an artificial floor.
|
|
52
|
+
spec.add_runtime_dependency("async", ">= 2.2")
|
|
49
53
|
spec.add_runtime_dependency("bundler", ">= 2.0")
|
|
50
54
|
spec.add_runtime_dependency("faraday-retry")
|
|
51
55
|
spec.add_runtime_dependency("gems")
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: still_active
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sean Floyd
|
|
@@ -127,14 +127,14 @@ dependencies:
|
|
|
127
127
|
requirements:
|
|
128
128
|
- - ">="
|
|
129
129
|
- !ruby/object:Gem::Version
|
|
130
|
-
version: '
|
|
130
|
+
version: '2.2'
|
|
131
131
|
type: :runtime
|
|
132
132
|
prerelease: false
|
|
133
133
|
version_requirements: !ruby/object:Gem::Requirement
|
|
134
134
|
requirements:
|
|
135
135
|
- - ">="
|
|
136
136
|
- !ruby/object:Gem::Version
|
|
137
|
-
version: '
|
|
137
|
+
version: '2.2'
|
|
138
138
|
- !ruby/object:Gem::Dependency
|
|
139
139
|
name: bundler
|
|
140
140
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -211,9 +211,11 @@ files:
|
|
|
211
211
|
- README.md
|
|
212
212
|
- bin/still_active
|
|
213
213
|
- lib/helpers/activity_helper.rb
|
|
214
|
+
- lib/helpers/alternatives_helper.rb
|
|
214
215
|
- lib/helpers/ansi_helper.rb
|
|
215
216
|
- lib/helpers/bot_context.rb
|
|
216
217
|
- lib/helpers/bundler_helper.rb
|
|
218
|
+
- lib/helpers/catalog_index.rb
|
|
217
219
|
- lib/helpers/cyclonedx_helper.rb
|
|
218
220
|
- lib/helpers/diff_markdown_helper.rb
|
|
219
221
|
- lib/helpers/emoji_helper.rb
|