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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +47 -0
- data/README.md +76 -45
- data/lib/helpers/activity_helper.rb +3 -1
- data/lib/helpers/bundler_helper.rb +30 -1
- data/lib/helpers/emoji_helper.rb +1 -1
- data/lib/helpers/health_score_helper.rb +81 -0
- data/lib/helpers/http_helper.rb +9 -2
- data/lib/helpers/libyear_helper.rb +20 -0
- data/lib/helpers/markdown_helper.rb +58 -10
- data/lib/helpers/ruby_helper.rb +83 -0
- data/lib/helpers/terminal_helper.rb +63 -7
- data/lib/helpers/version_helper.rb +1 -1
- data/lib/helpers/vulnerability_helper.rb +27 -0
- data/lib/still_active/cli.rb +31 -12
- data/lib/still_active/config.rb +4 -0
- data/lib/still_active/core_ext.rb +1 -1
- data/lib/still_active/deps_dev_client.rb +18 -0
- data/lib/still_active/gitlab_client.rb +31 -10
- data/lib/still_active/options.rb +13 -1
- data/lib/still_active/version.rb +1 -1
- data/lib/still_active/workflow.rb +126 -13
- data/lib/still_active.rb +4 -0
- data/still_active.gemspec +8 -4
- metadata +11 -23
- data/.github/dependabot.yml +0 -21
- data/.github/workflows/codeql-analysis.yml +0 -39
- data/.github/workflows/publish.yml +0 -19
- data/.github/workflows/rspec.yml +0 -26
- data/.github/workflows/rubocop-analysis.yml +0 -38
- data/.gitignore +0 -18
- data/.rspec +0 -4
- data/.rubocop.yml +0 -23
- data/Gemfile +0 -12
- data/Gemfile.lock +0 -239
- data/Rakefile +0 -12
- data/bin/console +0 -11
- data/bin/setup +0 -8
- data/fixtures/debug_versions.json +0 -38
- data/fixtures/still_active_version.json +0 -9
- data/fixtures/vcr_cassettes/deps_dev_project.yml +0 -46
- data/fixtures/vcr_cassettes/deps_dev_version.yml +0 -56
- data/fixtures/vcr_cassettes/gems.yml +0 -3762
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ebdc7af759c1dfe0bf4172000aab61f100e7dc8463374c5cf12458c69a3ff7ad
|
|
4
|
+
data.tar.gz: 5988d28e7fa84d686786ddf56e7da557d4935d8c5c305c33105593e859216941
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
[](https://badge.fury.io/rb/still_active)
|
|
8
8
|

|
|
@@ -10,16 +10,17 @@
|
|
|
10
10
|

|
|
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:
|
|
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
|
-
"
|
|
111
|
-
"
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
"
|
|
122
|
-
"
|
|
123
|
-
"
|
|
124
|
-
"
|
|
125
|
-
"
|
|
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
|
|
142
|
-
| -------- | ----------- | ------- | ----- |
|
|
143
|
-
|
|
|
144
|
-
| |
|
|
145
|
-
| |
|
|
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
|
|
169
|
+
Use exit-code flags to fail CI pipelines based on dependency health:
|
|
150
170
|
|
|
151
171
|
```bash
|
|
152
|
-
|
|
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 **
|
|
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
|
-
.
|
|
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
|
data/lib/helpers/emoji_helper.rb
CHANGED
|
@@ -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
|
data/lib/helpers/http_helper.rb
CHANGED
|
@@ -21,10 +21,17 @@ module StillActive
|
|
|
21
21
|
headers.each { |key, value| request[key] = value }
|
|
22
22
|
|
|
23
23
|
response = http.request(request)
|
|
24
|
-
|
|
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
|
|
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 =
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|