still_active 1.0.1 → 1.1.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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +47 -0
  3. data/README.md +76 -45
  4. data/lib/helpers/activity_helper.rb +3 -1
  5. data/lib/helpers/bundler_helper.rb +30 -1
  6. data/lib/helpers/emoji_helper.rb +1 -1
  7. data/lib/helpers/health_score_helper.rb +81 -0
  8. data/lib/helpers/http_helper.rb +9 -2
  9. data/lib/helpers/libyear_helper.rb +20 -0
  10. data/lib/helpers/markdown_helper.rb +58 -10
  11. data/lib/helpers/ruby_helper.rb +83 -0
  12. data/lib/helpers/terminal_helper.rb +63 -7
  13. data/lib/helpers/version_helper.rb +1 -1
  14. data/lib/helpers/vulnerability_helper.rb +27 -0
  15. data/lib/still_active/cli.rb +31 -12
  16. data/lib/still_active/config.rb +4 -0
  17. data/lib/still_active/core_ext.rb +1 -1
  18. data/lib/still_active/deps_dev_client.rb +18 -0
  19. data/lib/still_active/gitlab_client.rb +31 -10
  20. data/lib/still_active/options.rb +13 -1
  21. data/lib/still_active/version.rb +1 -1
  22. data/lib/still_active/workflow.rb +126 -13
  23. data/lib/still_active.rb +4 -0
  24. data/still_active.gemspec +8 -4
  25. metadata +11 -23
  26. data/.github/dependabot.yml +0 -21
  27. data/.github/workflows/codeql-analysis.yml +0 -39
  28. data/.github/workflows/publish.yml +0 -19
  29. data/.github/workflows/rspec.yml +0 -26
  30. data/.github/workflows/rubocop-analysis.yml +0 -38
  31. data/.gitignore +0 -18
  32. data/.rspec +0 -4
  33. data/.rubocop.yml +0 -23
  34. data/Gemfile +0 -12
  35. data/Gemfile.lock +0 -239
  36. data/Rakefile +0 -12
  37. data/bin/console +0 -11
  38. data/bin/setup +0 -8
  39. data/fixtures/debug_versions.json +0 -38
  40. data/fixtures/still_active_version.json +0 -9
  41. data/fixtures/vcr_cassettes/deps_dev_project.yml +0 -46
  42. data/fixtures/vcr_cassettes/deps_dev_version.yml +0 -56
  43. data/fixtures/vcr_cassettes/gems.yml +0 -3762
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f4f584514824888d1b457c3b3662c449d5aa5ef8244cb31760ed005afc2204d3
4
- data.tar.gz: 9e2c109064b073de04025c36730a171760beae79866e97bff44d4008d7cf1187
3
+ metadata.gz: ebdc7af759c1dfe0bf4172000aab61f100e7dc8463374c5cf12458c69a3ff7ad
4
+ data.tar.gz: 5988d28e7fa84d686786ddf56e7da557d4935d8c5c305c33105593e859216941
5
5
  SHA512:
6
- metadata.gz: 00a1c67f51fc48961185bfcab6f6ec1a30cfa15f417a8feec70606d673f2ac7278d7dd267376af27e6670d88316ba31ae7d8d63ad3f470f94b5c483e03cbbd1e
7
- data.tar.gz: bb4bc6f91c96a3f24381179cc295ea21f3f9545ef9a148f95fc53525195e6cda4111887c8d247c1bf50e59f2edd9e94ac5987609ab588300f3a612aae5228e5c
6
+ metadata.gz: a0c5bd6fabfc32a72dcb6169b89bfe3ffb93879117be4b3489705844f4d84ffc226ed4ba42b1bc86a2d253416de94503ffd3263777d172b1fb9d2fbc85ff687b
7
+ data.tar.gz: 05be629f02347fe1594351884a06135acf34d843a8611e073bce41fbae29164492bd343468cac5c6ed95651c775e2d93ea58d408c5879fd3f7253f2724954318
data/CHANGELOG.md CHANGED
@@ -1,5 +1,52 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.1.0] - 2026-02-20
4
+
5
+ ### Added
6
+
7
+ - `--ignore=GEM,GEM2,...` flag to exclude gems from pass/fail checks while keeping them in output
8
+ - `--fail-below-score=SCORE` flag for health-based CI gating (exit 1 if any gem scores below threshold)
9
+ - Yanked version detection: flags pinned versions that have been pulled from RubyGems
10
+ - Archived repo detection via GitHub and GitLab APIs, treated as critical for exit checks
11
+ - Libyear metric: years between installed and latest release per gem, total in summary
12
+ - Advisory enrichment: CVSS scores, titles, and IDs from deps.dev per vulnerability
13
+ - Composite health score (0-100) combining version freshness, activity, OpenSSF Scorecard, and vulnerabilities
14
+ - Health column in terminal and markdown output, system average in terminal summary
15
+ - Ruby version freshness: reports current Ruby version, EOL status, and libyear behind latest via endoflife.date API
16
+ - Source detection: identifies gem source type (rubygems, git, path) from Bundler lockfile
17
+ - Non-rubygems gem handling: git/path-sourced gems show gracefully with source indicator instead of failing silently
18
+ - GitHub Packages registry support: fetches versions from `rubygems.pkg.github.com` using existing `--github-oauth-token` (requires `read:packages` scope)
19
+ - CVSS v2 fallback: older advisories without v3 scores now show severity using v2 scores from deps.dev
20
+
21
+ ### Changed
22
+
23
+ - Vulnerability column shows count with highest severity label (e.g. "3 (critical)")
24
+ - Markdown vulnerability column shows advisory IDs
25
+ - Markdown table adds libyear and health columns
26
+ - Terminal summary includes libyear total and health average
27
+ - JSON output wrapped in `{ "gems": ..., "ruby": ... }` structure
28
+ - Version string validation guards against malformed versions from git-sourced gems
29
+ - Progress counter on stderr during gem checking so large Gemfiles don't appear frozen
30
+ - Actionable rate limit message when GitHub API quota is exhausted
31
+ - `--fail-below-score` now validates range (0-100) at parse time
32
+ - `--gems` option stores structured data from the start instead of mutating mid-run
33
+ - API failures (timeouts, HTTP errors, malformed responses) now warn on stderr instead of degrading silently
34
+ - Vulnerability count based on successfully fetched advisories so count and severity always agree
35
+
36
+ ### Fixed
37
+
38
+ - Vulnerability counts now checked against installed version, not latest (was masking CVEs in older pinned versions)
39
+ - `GitlabClient.archived?` returned `false` on API failure instead of `nil`, incorrectly asserting repos were not archived
40
+ - `repo_archived?` rescued all `StandardError`, masking bugs; now catches only `Octokit::Error` and `Faraday::Error`
41
+ - `last_commit_date` had no error handling; any failure dropped the entire gem from results
42
+ - Malformed date strings from GitHub/GitLab APIs no longer raise unhandled `ArgumentError`
43
+
44
+ ## [1.0.2] - 2026-02-19
45
+
46
+ ### Changed
47
+
48
+ - Reduce gem package from 2.4MB to essentials only (lib/, bin/still_active, LICENSE, README, CHANGELOG, gemspec)
49
+
3
50
  ## [1.0.1] - 2026-02-19
4
51
 
5
52
  ### Changed
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **How do you know if your Ruby dependencies are still maintained?**
4
4
 
5
- `bundle outdated` tells you version drift. `bundler-audit` catches known CVEs. Neither tells you whether anyone is still working on the thing. `still_active` checks maintenance activity, version freshness, security scores, and vulnerabilities for every gem in your Gemfile -- in one pass.
5
+ `bundle outdated` tells you version drift. `bundler-audit` catches known CVEs. Neither tells you whether anyone is still working on the thing. `still_active` checks maintenance activity, version freshness, security scores, vulnerabilities, libyear drift, and archived repos for every gem in your Gemfile -- with a composite health score per gem.
6
6
 
7
7
  [![Gem Version](https://badge.fury.io/rb/still_active.svg)](https://badge.fury.io/rb/still_active)
8
8
  ![Code Quality analysis](https://github.com/SeanLF/still_active/actions/workflows/codeql-analysis.yml/badge.svg)
@@ -10,16 +10,17 @@
10
10
  ![Rubocop analysis](https://github.com/SeanLF/still_active/actions/workflows/rubocop-analysis.yml/badge.svg)
11
11
 
12
12
  ```
13
- Name Version Activity OpenSSF Vulns
14
- ──────────────────────────────────────────────────────────────────
15
- code-scanning-rubocop 0.6.1 (latest) stale 3.1/10 0
16
- debug 1.11.1 (latest) ok 5.2/10 0
17
- faker 3.6.0 (latest) ok 7.4/10 0
18
- rake 13.3.1 (latest) ok 5.3/10 0
19
- rspec 3.13.2 (latest) ok 6.9/10 0
20
- rubocop 1.84.2 (latest) ok 5.9/10 0
21
-
22
- 12 gems: 12 up to date, 0 outdated · 11 active, 1 stale · 0 vulnerabilities
13
+ Name Version Activity OpenSSF Vulns Health
14
+ ───────────────────────────────────────────────────────────────────────────
15
+ code-scanning-rubocop 0.6.1 (latest) stale 3.1/10 0 71/100
16
+ debug 1.11.1 (latest) ok 5.2/10 0 90/100
17
+ faker 3.6.0 (latest) ok 7.4/10 0 95/100
18
+ rake 13.3.1 (latest) ok 5.3/10 0 91/100
19
+ rspec 3.13.2 (latest) ok 6.9/10 0 94/100
20
+ rubocop 1.84.2 (latest) ok 5.9/10 0 92/100
21
+
22
+ 12 gems: 11 up to date, 0 outdated · 11 active, 1 stale · 0 vulnerabilities · health 93/100
23
+ Ruby 4.0.1 (latest)
23
24
  ```
24
25
 
25
26
  ## Why `still_active`?
@@ -29,11 +30,17 @@ Most dependency tools answer one question. `still_active` answers all of them at
29
30
  | | `bundle outdated` | `bundler-audit` | `libyear-bundler` | **`still_active`** |
30
31
  | ---------------------------- | ----------------- | --------------- | ----------------- | ---------------------------- |
31
32
  | Outdated versions | Yes | - | Yes | **Yes** |
32
- | Known vulnerabilities (CVEs) | - | Yes | - | **Yes** |
33
+ | Known vulnerabilities (CVEs) | - | Yes | - | **Yes** (with severity) |
33
34
  | OpenSSF Scorecard | - | - | - | **Yes** |
34
35
  | Last commit activity | - | - | - | **Yes** |
36
+ | Libyear drift | - | - | Yes | **Yes** |
37
+ | Composite health score | - | - | - | **Yes** (0-100) |
38
+ | Archived repo detection | - | - | - | **Yes** |
39
+ | Yanked version detection | - | - | - | **Yes** |
40
+ | Ruby version freshness | - | - | - | **Yes** (EOL + libyear) |
41
+ | Git/path/GH Packages sources | - | - | - | **Yes** |
35
42
  | GitLab support | - | - | - | **Yes** |
36
- | CI quality gates | - | Exit code | - | **Yes** |
43
+ | CI quality gates | - | Exit code | - | **Yes** (4 modes) |
37
44
  | Multiple output formats | - | - | - | **Terminal, JSON, Markdown** |
38
45
  | Single command | Yes | Yes | Yes | **Yes** |
39
46
 
@@ -54,8 +61,11 @@ still_active
54
61
  # check specific gems
55
62
  still_active --gems=rails,nokogiri,sidekiq
56
63
 
57
- # CI pipeline: fail if any gem is critically stale
58
- still_active --fail-if-critical
64
+ # CI pipeline: fail if any gem is critically stale or has low health
65
+ still_active --fail-if-critical --fail-below-score=50
66
+
67
+ # ignore specific gems in CI checks
68
+ still_active --fail-if-warning --ignore=legacy_gem,internal_gem
59
69
 
60
70
  # markdown table for pull requests or documentation
61
71
  still_active --markdown
@@ -86,6 +96,8 @@ Usage: still_active [options]
86
96
  --warning-range-end=YEARS maximum years since last activity that triggers a warning (beyond this is critical)
87
97
  --fail-if-critical Exit 1 if any gem has critical activity warning
88
98
  --fail-if-warning Exit 1 if any gem has warning or critical activity warning
99
+ --fail-below-score=SCORE Exit 1 if any gem health score is below threshold
100
+ --ignore=GEM,GEM2,... Exclude gems from pass/fail checks (still shown in output)
89
101
  --critical-warning-emoji=EMOJI
90
102
  --futurist-emoji=EMOJI
91
103
  --success-emoji=EMOJI
@@ -107,27 +119,33 @@ still_active --json --gems=rails,nokogiri
107
119
 
108
120
  ```json
109
121
  {
110
- "rails": {
111
- "latest_version": "8.1.2",
112
- "latest_version_release_date": "2026-01-08 20:18:51 UTC",
113
- "latest_pre_release_version": "8.1.0.rc1",
114
- "latest_pre_release_version_release_date": "2025-10-15 00:52:14 UTC",
115
- "repository_url": "https://github.com/rails/rails",
116
- "last_commit_date": "2026-02-19 09:39:03 UTC",
117
- "scorecard_score": 5.7,
118
- "vulnerability_count": 0,
119
- "ruby_gems_url": "https://rubygems.org/gems/rails"
122
+ "gems": {
123
+ "rails": {
124
+ "source_type": "rubygems",
125
+ "latest_version": "8.1.2",
126
+ "repository_url": "https://github.com/rails/rails",
127
+ "last_commit_date": "2026-02-19 09:39:03 UTC",
128
+ "archived": false,
129
+ "scorecard_score": 5.7,
130
+ "vulnerability_count": 0,
131
+ "health_score": 88
132
+ },
133
+ "nokogiri": {
134
+ "source_type": "rubygems",
135
+ "latest_version": "1.19.1",
136
+ "repository_url": "https://github.com/sparklemotion/nokogiri",
137
+ "last_commit_date": "2026-02-17 19:13:22 UTC",
138
+ "archived": false,
139
+ "scorecard_score": 6.5,
140
+ "vulnerability_count": 0,
141
+ "health_score": 90
142
+ }
120
143
  },
121
- "nokogiri": {
122
- "latest_version": "1.19.1",
123
- "latest_version_release_date": "2026-02-16 23:31:21 UTC",
124
- "latest_pre_release_version": "1.18.0.rc1",
125
- "latest_pre_release_version_release_date": "2024-12-16 17:48:44 UTC",
126
- "repository_url": "https://github.com/sparklemotion/nokogiri",
127
- "last_commit_date": "2026-02-17 19:13:22 UTC",
128
- "scorecard_score": 6.5,
129
- "vulnerability_count": 0,
130
- "ruby_gems_url": "https://rubygems.org/gems/nokogiri"
144
+ "ruby": {
145
+ "version": "4.0.1",
146
+ "eol": false,
147
+ "latest_version": "4.0.1",
148
+ "libyear": 0.0
131
149
  }
132
150
  }
133
151
  ```
@@ -138,18 +156,30 @@ still_active --json --gems=rails,nokogiri
138
156
  still_active --markdown
139
157
  ```
140
158
 
141
- | activity | up to date? | OpenSSF | vulns | name | version used | latest version | latest pre-release | last commit |
142
- | -------- | ----------- | ------- | ----- | --------------------- | ---------------- | ---------------- | ------------------- | ----------- |
143
- | ⚠️ | | 3.1/10 | ✅ | code-scanning-rubocop | 0.6.1 (2022/02) | 0.6.1 (2022/02) | | 2024/06 |
144
- | | | 5.2/10 | ✅ | debug | 1.11.1 (2025/12) | 1.11.1 (2025/12) | 1.0.0.rc2 (2021/09) | 2025/12 |
145
- | | | 7.4/10 | ✅ | faker | 3.6.0 (2026/01) | 3.6.0 (2026/01) | ❓ | 2026/02 |
159
+ | activity | up to date? | OpenSSF | vulns | name | version used | latest version | latest pre-release | last commit | libyear | health |
160
+ | -------- | ----------- | ------- | ----- | -------- | ------------ | ---------------- | -------------------- | ----------- | ------- | ------ |
161
+ | | | 5.2/10 | ✅ | debug | | 1.11.1 (2025/12) | 1.0.0.rc2 (2021/09) | 2025/12 | - | 86/100 |
162
+ | | | 6.5/10 | ✅ | nokogiri | | 1.19.1 (2026/02) | 1.18.0.rc1 (2024/12) | 2026/02 | - | 90/100 |
163
+ | | | 5.7/10 | ✅ | rails | ❓ | 8.1.2 (2026/01) | 8.1.0.rc1 (2025/10) | 2026/02 | - | 88/100 |
164
+
165
+ **Ruby 4.0.1** (latest) ✅
146
166
 
147
167
  ### CI quality gating
148
168
 
149
- Use `--fail-if-critical` or `--fail-if-warning` to fail CI pipelines when dependencies exceed activity thresholds:
169
+ Use exit-code flags to fail CI pipelines based on dependency health:
150
170
 
151
171
  ```bash
152
- still_active --gemfile=Gemfile --fail-if-warning --json
172
+ # fail on critically stale or archived gems
173
+ still_active --fail-if-critical --json
174
+
175
+ # fail on any stale, critical, or archived gem
176
+ still_active --fail-if-warning --json
177
+
178
+ # fail if any gem's health score drops below a threshold
179
+ still_active --fail-below-score=50 --json
180
+
181
+ # combine flags and exclude known exceptions
182
+ still_active --fail-if-warning --fail-below-score=50 --ignore=legacy_gem --json
153
183
  ```
154
184
 
155
185
  ### Activity thresholds
@@ -162,9 +192,10 @@ Activity is determined by the most recent signal across last commit date, latest
162
192
 
163
193
  ### Data sources
164
194
 
165
- - **Versions and release dates** from [RubyGems.org](https://rubygems.org)
166
- - **Last commit date** from the [GitHub](https://docs.github.com/en/rest) or [GitLab](https://docs.gitlab.com/ee/api/) API
167
- - **OpenSSF Scorecard** and **vulnerability counts** from Google's [deps.dev](https://deps.dev) API
195
+ - **Versions and release dates** from [RubyGems.org](https://rubygems.org) or [GitHub Packages](https://docs.github.com/en/packages)
196
+ - **Last commit date and archived status** from the [GitHub](https://docs.github.com/en/rest) or [GitLab](https://docs.gitlab.com/ee/api/) API
197
+ - **OpenSSF Scorecard**, **vulnerability counts**, and **CVSS severity** from Google's [deps.dev](https://deps.dev) API
198
+ - **Ruby version freshness** from [endoflife.date](https://endoflife.date)
168
199
 
169
200
  ### Configuration defaults
170
201
 
@@ -8,8 +8,10 @@ module StillActive
8
8
 
9
9
  using StillActive::CoreExt
10
10
 
11
- # Returns :ok, :stale, :critical, or :unknown
11
+ # Returns :archived, :ok, :stale, :critical, or :unknown
12
12
  def activity_level(gem_data)
13
+ return :archived if gem_data[:archived]
14
+
13
15
  most_recent = [
14
16
  gem_data[:last_commit_date],
15
17
  gem_data[:latest_version_release_date],
@@ -12,7 +12,36 @@ module StillActive
12
12
  .locked_gems
13
13
  .specs
14
14
  .select { |spec| gemfile_gems.include?(spec.name) }
15
- .each_with_object([]) { |spec, array| array << { name: spec.name, version: spec.version.version } }
15
+ .map do |spec|
16
+ {
17
+ name: spec.name,
18
+ version: spec.version.version,
19
+ source_type: detect_source_type(spec),
20
+ source_uri: detect_source_uri(spec),
21
+ }
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def detect_source_type(spec)
28
+ case spec.source
29
+ when ::Bundler::Source::Rubygems then :rubygems
30
+ when ::Bundler::Source::Git then :git
31
+ when ::Bundler::Source::Path then :path
32
+ else :unknown
33
+ end
34
+ end
35
+
36
+ def detect_source_uri(spec)
37
+ case spec.source
38
+ when ::Bundler::Source::Rubygems
39
+ spec.source.remotes&.first&.to_s
40
+ when ::Bundler::Source::Git
41
+ spec.source.uri
42
+ when ::Bundler::Source::Path
43
+ spec.source.path&.to_s
44
+ end
16
45
  end
17
46
  end
18
47
  end
@@ -10,7 +10,7 @@ module StillActive
10
10
  case ActivityHelper.activity_level(result_hash)
11
11
  when :ok then ""
12
12
  when :stale then StillActive.config.warning_emoji
13
- when :critical then StillActive.config.critical_warning_emoji
13
+ when :archived, :critical then StillActive.config.critical_warning_emoji
14
14
  when :unknown then StillActive.config.unsure_emoji
15
15
  end
16
16
  end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "activity_helper"
4
+
5
+ module StillActive
6
+ module HealthScoreHelper
7
+ extend self
8
+
9
+ WEIGHTS = {
10
+ version_freshness: 30,
11
+ activity: 25,
12
+ scorecard: 20,
13
+ vulnerabilities: 25,
14
+ }.freeze
15
+
16
+ def gem_score(gem_data)
17
+ components = {
18
+ version_freshness: version_freshness_score(gem_data),
19
+ activity: activity_score(gem_data),
20
+ scorecard: scorecard_score(gem_data),
21
+ vulnerabilities: vulnerability_score(gem_data),
22
+ }
23
+
24
+ available = components.compact
25
+ return if available.empty?
26
+
27
+ total_weight = available.keys.sum { |k| WEIGHTS[k] }
28
+ weighted_sum = available.sum { |k, v| WEIGHTS[k] * v }
29
+ (weighted_sum.to_f / total_weight).round
30
+ end
31
+
32
+ def system_average(result)
33
+ scores = result.each_value.filter_map { |d| d[:health_score] }
34
+ return if scores.empty?
35
+
36
+ (scores.sum.to_f / scores.size).round
37
+ end
38
+
39
+ private
40
+
41
+ def version_freshness_score(gem_data)
42
+ return 0 if gem_data[:version_yanked]
43
+
44
+ libyear = gem_data[:libyear]
45
+ return if libyear.nil?
46
+
47
+ [100 - (libyear * 20), 0].max.round
48
+ end
49
+
50
+ def activity_score(gem_data)
51
+ return 0 if gem_data[:archived]
52
+
53
+ level = ActivityHelper.activity_level(gem_data)
54
+ case level
55
+ when :ok then 100
56
+ when :stale then 40
57
+ when :critical then 10
58
+ when :unknown then nil
59
+ end
60
+ end
61
+
62
+ def scorecard_score(gem_data)
63
+ score = gem_data[:scorecard_score]
64
+ return if score.nil?
65
+
66
+ (score * 10).round
67
+ end
68
+
69
+ def vulnerability_score(gem_data)
70
+ count = gem_data[:vulnerability_count]
71
+ return if count.nil?
72
+
73
+ case count
74
+ when 0 then 100
75
+ when 1 then 40
76
+ when 2 then 20
77
+ else 0
78
+ end
79
+ end
80
+ end
81
+ end
@@ -21,10 +21,17 @@ module StillActive
21
21
  headers.each { |key, value| request[key] = value }
22
22
 
23
23
  response = http.request(request)
24
- return unless response.is_a?(Net::HTTPSuccess)
24
+ unless response.is_a?(Net::HTTPSuccess)
25
+ $stderr.puts("warning: #{uri.host}#{uri.path} returned HTTP #{response.code}") unless response.is_a?(Net::HTTPNotFound)
26
+ return
27
+ end
25
28
 
26
29
  JSON.parse(response.body)
27
- rescue Net::OpenTimeout, Net::ReadTimeout, SocketError, Errno::ECONNREFUSED, JSON::ParserError
30
+ rescue Net::OpenTimeout, Net::ReadTimeout, SocketError, Errno::ECONNREFUSED => e
31
+ $stderr.puts("warning: #{uri.host}#{uri.path} failed: #{e.class} (#{e.message})")
32
+ nil
33
+ rescue JSON::ParserError => e
34
+ $stderr.puts("warning: #{uri.host}#{uri.path} returned invalid JSON: #{e.message}")
28
35
  nil
29
36
  end
30
37
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../still_active/core_ext"
4
+
5
+ module StillActive
6
+ module LibyearHelper
7
+ extend self
8
+
9
+ def gem_libyear(version_used_release_date:, latest_version_release_date:)
10
+ return if version_used_release_date.nil? || latest_version_release_date.nil?
11
+
12
+ diff = latest_version_release_date - version_used_release_date
13
+ [diff / CoreExt::SECONDS_PER_YEAR, 0.0].max.round(1)
14
+ end
15
+
16
+ def total_libyear(result)
17
+ result.each_value.sum { |d| d[:libyear] || 0.0 }
18
+ end
19
+ end
20
+ end
@@ -1,12 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "vulnerability_helper"
4
+
3
5
  module StillActive
4
6
  module MarkdownHelper
5
7
  extend self
6
8
 
9
+ def ruby_line(ruby_info)
10
+ version = ruby_info[:version]
11
+ latest = ruby_info[:latest_version]
12
+ libyear = ruby_info[:libyear]
13
+ eol = ruby_info[:eol]
14
+ eol_date = ruby_info[:eol_date]
15
+
16
+ return "**Ruby #{version}** (latest) #{StillActive.config.success_emoji}" if version == latest
17
+
18
+ libyear_part = libyear ? "#{libyear} libyears behind #{latest}" : "behind #{latest}"
19
+
20
+ if eol
21
+ eol_part = eol_date ? "EOL #{eol_date.strftime("%Y-%m-%d")}" : "EOL"
22
+ "**Ruby #{version}** (#{eol_part}, #{libyear_part}) #{StillActive.config.critical_warning_emoji}"
23
+ else
24
+ "**Ruby #{version}** (#{libyear_part}) #{StillActive.config.warning_emoji}"
25
+ end
26
+ end
27
+
7
28
  def markdown_table_header_line
8
- "| activity | up to date? | OpenSSF | vulns | name | version used | latest version | latest pre-release | last commit |\n" \
9
- "| -------- | ----------- | ------- | ----- | ---- | ------------ | -------------- | ------------------ | ----------- |"
29
+ "| activity | up to date? | OpenSSF | vulns | name | version used | latest version | latest pre-release | last commit | libyear | health |\n" \
30
+ "| -------- | ----------- | ------- | ----- | ---- | ------------ | -------------- | ------------------ | ----------- | ------- | ------ |"
10
31
  end
11
32
 
12
33
  def markdown_table_body_line(gem_name:, data:)
@@ -18,11 +39,17 @@ module StillActive
18
39
 
19
40
  formatted_name = markdown_url(text: gem_name, url: repository_url)
20
41
 
21
- formatted_version_used = version_with_date(
22
- text: data[:version_used],
23
- url: version_url(ruby_gems_url, data[:version_used]),
24
- date: data[:version_used_release_date],
25
- )
42
+ formatted_version_used = if [:git, :path].include?(data[:source_type])
43
+ data[:version_used] ? "#{data[:version_used]} (#{data[:source_type]})" : "(#{data[:source_type]})"
44
+ elsif data[:version_yanked]
45
+ "#{data[:version_used]} (YANKED #{StillActive.config.critical_warning_emoji})"
46
+ else
47
+ version_with_date(
48
+ text: data[:version_used],
49
+ url: version_url(ruby_gems_url, data[:version_used]),
50
+ date: data[:version_used_release_date],
51
+ )
52
+ end
26
53
 
27
54
  formatted_latest_version = version_with_date(
28
55
  text: data[:latest_version],
@@ -44,12 +71,14 @@ module StillActive
44
71
  inactive_repository_emoji || unsure,
45
72
  using_latest_version_emoji || unsure,
46
73
  format_scorecard(data[:scorecard_score]),
47
- format_vulns(data[:vulnerability_count]),
74
+ format_vulns(data),
48
75
  formatted_name,
49
76
  formatted_version_used || unsure,
50
77
  formatted_latest_version || unsure,
51
78
  formatted_latest_pre_release || unsure,
52
79
  formatted_last_commit || unsure,
80
+ format_libyear(data[:libyear]),
81
+ format_health(data[:health_score]),
53
82
  ]
54
83
 
55
84
  "| #{cells.join(" | ")} |"
@@ -79,11 +108,30 @@ module StillActive
79
108
  "#{score}/10"
80
109
  end
81
110
 
82
- def format_vulns(count)
111
+ def format_health(score)
112
+ return "-" if score.nil?
113
+
114
+ "#{score}/100"
115
+ end
116
+
117
+ def format_libyear(value)
118
+ return "-" if value.nil?
119
+
120
+ "#{value}y"
121
+ end
122
+
123
+ def format_vulns(data)
124
+ count = data[:vulnerability_count]
83
125
  return StillActive.config.unsure_emoji if count.nil?
84
126
  return StillActive.config.success_emoji if count.zero?
85
127
 
86
- count.to_s
128
+ vulnerabilities = data[:vulnerabilities] || []
129
+ severity = VulnerabilityHelper.highest_severity(vulnerabilities)
130
+ ids = vulnerabilities.flat_map { |v| [v[:id], *v[:aliases]] }.compact.uniq.first(3)
131
+
132
+ parts = [severity ? "#{count} (#{severity})" : count.to_s]
133
+ parts << ids.join(", ") unless ids.empty?
134
+ parts.join(" ")
87
135
  end
88
136
 
89
137
  def markdown_url(text:, url:)
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+ require_relative "http_helper"
5
+ require_relative "libyear_helper"
6
+
7
+ module StillActive
8
+ module RubyHelper
9
+ extend self
10
+
11
+ ENDOFLIFE_URI = URI("https://endoflife.date/")
12
+
13
+ def ruby_freshness
14
+ return unless standard_ruby?
15
+
16
+ cycles = fetch_cycles
17
+ return if cycles.nil?
18
+
19
+ current = current_ruby_version
20
+ current_cycle = find_cycle(cycles, current)
21
+ latest_cycle = cycles.first
22
+
23
+ return if latest_cycle.nil?
24
+
25
+ latest_version = latest_cycle["latest"]
26
+ latest_release_date = parse_date(latest_cycle["releaseDate"])
27
+ current_release_date = parse_date(current_cycle&.dig("releaseDate"))
28
+ eol_value = current_cycle&.dig("eol")
29
+
30
+ {
31
+ version: current,
32
+ release_date: current_release_date,
33
+ eol_date: parse_eol(eol_value),
34
+ eol: eol_reached?(eol_value),
35
+ latest_version: latest_version,
36
+ latest_release_date: latest_release_date,
37
+ libyear: LibyearHelper.gem_libyear(
38
+ version_used_release_date: current_release_date,
39
+ latest_version_release_date: latest_release_date,
40
+ ),
41
+ }
42
+ end
43
+
44
+ private
45
+
46
+ def standard_ruby?
47
+ RUBY_ENGINE == "ruby"
48
+ end
49
+
50
+ def current_ruby_version
51
+ RUBY_VERSION
52
+ end
53
+
54
+ def fetch_cycles
55
+ HttpHelper.get_json(ENDOFLIFE_URI, "/api/ruby.json")
56
+ end
57
+
58
+ def find_cycle(cycles, version)
59
+ major_minor = version.split(".")[0..1].join(".")
60
+ cycles.find { |c| c["cycle"] == major_minor }
61
+ end
62
+
63
+ def parse_date(date_string)
64
+ return if date_string.nil?
65
+
66
+ Time.parse(date_string)
67
+ end
68
+
69
+ def parse_eol(value)
70
+ case value
71
+ when String then parse_date(value)
72
+ end
73
+ end
74
+
75
+ def eol_reached?(value)
76
+ case value
77
+ when true then true
78
+ when false then false
79
+ when String then Time.parse(value) <= Time.now
80
+ end
81
+ end
82
+ end
83
+ end