still_active 3.0.0.rc1 → 3.0.0.rc2
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 +1 -0
- data/README.md +102 -402
- data/lib/helpers/version_helper.rb +18 -0
- data/lib/still_active/version.rb +1 -1
- data/lib/still_active/workflow.rb +4 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 89af276523def67c4566cdcc8ff084c14383f4c43dff3429a739f861cfea8555
|
|
4
|
+
data.tar.gz: 8a6da7245242d2778126ed993484fd5bddf0cfba66d612cc120c705c1c519383
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b3fd52ed8f8ad9a05267c22074b3720f9bdc48d0e65e0ff557a56c9d9f43cee7c9f07d1ec7acca2f82012a34a7c181d10a23d2427edd245d7055dad9e8866c4b
|
|
7
|
+
data.tar.gz: 3751290569ec6bbd5e0d7cb9da9e76ebdd8add640c2baca94f04488b4f0874a8407ae9d5ceffecde0cc1296552aaaf765e3bb96f6e7e80e13fcf79a9e65471f3
|
data/CHANGELOG.md
CHANGED
|
@@ -33,6 +33,7 @@ The new `--fail-if-poison[=TIER]` and `--fail-if-language-ceiling[=TIER]` gates
|
|
|
33
33
|
|
|
34
34
|
### Fixed
|
|
35
35
|
|
|
36
|
+
- **A stale pre-release no longer corrupts the up-to-date signal.** A pre-release older than the latest stable (an `8.1.0.rc1` still listed after `8.1.2` shipped, or a decade-old `rc` on a gem long past it) was treated as a live upgrade target: `up_to_date` compares the version in hand against it, so any current version read as up to date and a gem that was actually behind got the "futurist" marker, while the pre-release column filled with superseded rcs. The latest pre-release is now kept only when strictly newer than the latest stable (or when there is no stable release at all, where it is the only signal), so the column, the `up_to_date` field, and the emoji agree. Genuine upcoming pre-releases still surface. On a real bundle this was mislabelling roughly twenty gems.
|
|
36
37
|
- **`--baseline` now honours a committed `.still_active.yml`.** The PR-diff gate was the only gate that ignored the suppression list: a maintenance regression the audit and SARIF gates already accept still tripped `--baseline` the moment the dependency was added, with no way to accept it short of merging it to the baseline first. An accepted regression is now moved out of the CI-failable set and shown under an "Accepted (suppressed via `.still_active.yml`)" section with its reason, so the acceptance stays visible rather than silently vanishing. Only maintenance kinds a bare gem+signal entry can cover are accepted (archived/staleness map to `activity`/`libyear`); a newly introduced vulnerability still fails, since suppressing one needs an explicit advisory id the diff regression doesn't carry.
|
|
37
38
|
- **The fail-open on the vulnerability gate is closed on both paths.** `--fail-if-vulnerable=<severity>` no longer passes a confirmed advisory that carries no CVSS score (an unscored advisory read as "below threshold" and cleared the gate, worst for a freshly disclosed CVE); it now fails closed with a per-gem stderr note (see Upgrading). And the native path no longer drops a confirmed advisory when its deps.dev detail fetch fails (429/timeout/5xx); it previously `filter_map`'d the failure away and zeroed `vulnerability_count`, it now keeps a minimal advisory so the finding survives and the fail-closed logic applies.
|
|
38
39
|
- **deps.dev CVE aliases are surfaced correctly.** The v3alpha API returns aliases as bare id strings, not objects; the parser read `a["id"]` on a string and dropped every alias, so cross-referenced CVE/GHSA ids vanished. It now tolerates both shapes (and coerces a drifted non-string alias to a string, warning once, so it can't crash the advisory merge and read a vulnerable gem as clean).
|
data/README.md
CHANGED
|
@@ -1,54 +1,45 @@
|
|
|
1
1
|
# `still_active`
|
|
2
2
|
|
|
3
|
-
**How do you know
|
|
3
|
+
**How do you know the dependencies you ship are still maintained?**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[Install](#installation) · [Quick start](#quick-start) · [GitHub Action & CI](#github-action--ci) · [Cross-ecosystem](#cross-ecosystem-audit) · [Configuration](#configuration) · [Rules](docs/rules.md)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Your package manager tells you what's outdated and what has a known CVE. Neither tells you whether anyone's still working on the thing. An abandoned dependency is a quiet liability: when it finally breaks or a vulnerability lands, there's no release coming and no one to ping, and you find out at the worst time.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
[](https://badge.fury.io/rb/still_active)
|
|
12
|
-
[](https://github.com/marketplace/actions/still_active)
|
|
13
|
-

|
|
14
|
-

|
|
15
|
-

|
|
9
|
+
`still_active` flags that risk before it bites, across your whole dependency graph: archived repos, no release in years, low OpenSSF scores, unfixable vulnerabilities, and the poison-pill case where a dormant package holds one of your deps below its own security patch. Native on **Ruby** gems; **npm, PyPI, Cargo, Go, Maven, and NuGet** via a CycloneDX SBOM.
|
|
16
10
|
|
|
17
11
|
```
|
|
18
|
-
Name
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
7 gems: 4 up to date, 0 outdated · 2 active, 2 stale, 2 archived · 0 vulnerabilities
|
|
29
|
-
Ruby 4.0.1 (latest)
|
|
12
|
+
Name Version Activity OpenSSF Vulns License
|
|
13
|
+
────────────────────────────────────────────────────────────────────
|
|
14
|
+
backbone-rails 1.2.3 (latest) archived 2.6/10 0 -
|
|
15
|
+
nokogiri 1.19.4 (latest) ok 5.9/10 0 MIT
|
|
16
|
+
paperclip 6.1.0 (latest) archived 2/10 0 MIT
|
|
17
|
+
↳ poison: caps terrapin ~> 0.6.0 (1 major behind, latest 1.x)
|
|
18
|
+
rake 13.4.2 (latest) ok 5.5/10 0 MIT
|
|
19
|
+
|
|
20
|
+
4 gems: 4 up to date · 2 active, 2 archived · 0 vulnerabilities · 1 poison-pill
|
|
21
|
+
Ruby 4.0.5 (latest)
|
|
30
22
|
```
|
|
31
23
|
|
|
32
24
|
## Why `still_active`?
|
|
33
25
|
|
|
34
|
-
|
|
26
|
+
No package ecosystem's standard tooling answers "is anyone still maintaining this?" `npm audit`, `cargo audit`, `pip-audit`, and `bundler-audit` find known CVEs; the `outdated` commands find version drift. None tell you a dependency's upstream is archived, hasn't shipped in three years, or is holding one of your deps below its security fix. That maintenance gap is what `still_active` fills. It's **complementary to**, not a replacement for, the tools you already run.
|
|
27
|
+
|
|
28
|
+
On Ruby, where it runs natively:
|
|
35
29
|
|
|
36
30
|
| | `bundle outdated` | `bundler-audit` | `libyear-bundler` | **`still_active`** |
|
|
37
31
|
| ---------------------------- | ----------------- | ---------------------- | ----------------- | ------------------------ |
|
|
38
32
|
| Outdated versions | Yes | - | Yes | Yes |
|
|
39
33
|
| Known vulnerabilities (CVEs) | - | Yes (ruby-advisory-db) | - | Yes (deps.dev + OSV + ruby-advisory-db) |
|
|
40
34
|
| Libyear drift | - | - | Yes | Yes |
|
|
41
|
-
| **Last
|
|
35
|
+
| **Last-commit activity** | - | - | - | **Yes** |
|
|
42
36
|
| **Archived repo detection** | - | - | - | **Yes** |
|
|
43
37
|
| **OpenSSF Scorecard** | - | - | - | **Yes** |
|
|
44
|
-
| **
|
|
45
|
-
| **Ruby version freshness** | - | - | - | **Yes** (EOL + libyear) |
|
|
38
|
+
| **Poison-pill / below-the-fix** | - | - | - | **Yes** |
|
|
46
39
|
| **Cross-ecosystem** (npm/PyPI/Cargo/Go/Maven/NuGet) | - | - | - | **Yes** (`--sbom`) |
|
|
47
|
-
|
|
|
48
|
-
| CI quality gates | - | Exit code | - | Yes (6 flags) |
|
|
49
|
-
| Output formats | Text | Text | Text | Terminal, JSON, Markdown, SARIF, CycloneDX |
|
|
40
|
+
| CI quality gates | - | Exit code | - | Yes (6 gate flags) |
|
|
50
41
|
|
|
51
|
-
|
|
42
|
+
With `bundler-audit` installed alongside, `still_active` merges its `ruby-advisory-db` advisories with its own deps.dev + OSV sources, so running both no longer means reconciling two vuln counts by hand.
|
|
52
43
|
|
|
53
44
|
## Installation
|
|
54
45
|
|
|
@@ -56,218 +47,29 @@ The bolded rows are the gap `still_active` fills: nobody else answers "is the ma
|
|
|
56
47
|
gem install still_active
|
|
57
48
|
```
|
|
58
49
|
|
|
59
|
-
|
|
50
|
+
Requires an actively-maintained Ruby (the gemspec floor tracks Ruby's [EOL schedule](https://endoflife.date/ruby)). You don't have to run it *on* the Ruby you audit: it reports on the version pinned in `Gemfile.lock`, so run it from any current Ruby and it still flags an EOL target.
|
|
60
51
|
|
|
61
|
-
## Quick
|
|
52
|
+
## Quick start
|
|
62
53
|
|
|
63
54
|
```bash
|
|
64
55
|
# audit your Gemfile (auto-detects output format)
|
|
65
56
|
still_active
|
|
66
57
|
|
|
67
|
-
#
|
|
68
|
-
still_active --
|
|
58
|
+
# cross-ecosystem: audit any CycloneDX SBOM (npm, PyPI, Cargo, Go, Maven, NuGet)
|
|
59
|
+
syft dir:. -o cyclonedx-json > sbom.json && still_active --sbom=sbom.json
|
|
69
60
|
|
|
70
|
-
# CI
|
|
61
|
+
# fail CI if any gem is critically stale or vulnerable
|
|
71
62
|
still_active --fail-if-critical --fail-if-vulnerable
|
|
72
63
|
|
|
73
|
-
#
|
|
74
|
-
still_active --fail-if-warning --ignore=legacy_gem,internal_gem
|
|
75
|
-
|
|
76
|
-
# markdown table for pull requests or documentation
|
|
77
|
-
still_active --markdown
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
## Usage
|
|
81
|
-
|
|
82
|
-
### Authentication
|
|
83
|
-
|
|
84
|
-
`still_active` discovers a GitHub token in this order:
|
|
85
|
-
|
|
86
|
-
1. `--github-oauth-token=TOKEN` CLI flag
|
|
87
|
-
2. `GITHUB_TOKEN` environment variable (CI convention)
|
|
88
|
-
3. `GH_TOKEN` environment variable (`gh` CLI convention)
|
|
89
|
-
4. `gh auth token` (if `gh` is installed and authenticated)
|
|
90
|
-
|
|
91
|
-
With a token, GitHub repo signals (archived, last commit) come from the GitHub API (limit 5000 requests/hour). When a rate-limit response comes back with a reset that's near (within 60 seconds, typically a secondary/burst limit that the concurrent fan-out can trip even with a token), still_active waits it out and retries rather than dropping that gem's signals; a far-off reset (you've exhausted the hourly limit) isn't waited out, so run less often.
|
|
92
|
-
|
|
93
|
-
**Without a token**, the unauthenticated GitHub API is capped at 60 requests/hour — unusable past a handful of gems. So still_active falls back to [ecosyste.ms](https://ecosyste.ms) for GitHub repo signals (5000 anonymous requests/hour), which keeps a large Gemfile working with no token at all. The fallback is GitHub-only: ecosyste.ms doesn't track commit recency for GitLab/Codeberg, so those hosts still need their own token (or report `unknown`) when unauthenticated. A token still gives the freshest data and unlocks `--unreleased-commits`, so prefer one in CI. To be a good citizen of the free ecosyste.ms service, you can join its "polite pool" (a higher rate limit, and they can reach you) by passing a contact email via `--ecosystems-email=you@example.com` or `STILL_ACTIVE_ECOSYSTEMS_EMAIL` — optional, since the anonymous limit already covers a typical lockfile.
|
|
94
|
-
|
|
95
|
-
GitLab cascade mirrors GitHub: `--gitlab-token` → `GITLAB_TOKEN` → `glab auth status --show-token`. Optional for public repos, required for private ones.
|
|
96
|
-
|
|
97
|
-
Forgejo/Codeberg (the rare gem whose canonical `source_code_uri` is `codeberg.org`) is read anonymously by default; set `STILL_ACTIVE_FORGEJO_TOKEN` (or `CODEBERG_TOKEN`) only to raise the rate limit or reach a private repo. There is no CLI flag, since Codeberg has no ubiquitous CLI to borrow a token from the way `gh`/`glab` do.
|
|
98
|
-
|
|
99
|
-
Artifactory auth looks for Bundler configuration's credentials first so that private registries work without extra configuration if they are already configured in Bundler. To set those credentials in Bundler, run `bundle config set credentials.my-org.jfrog.io user:pass`. If no credentials are set there, still_active falls back using a token provided via `--artifactory-token` or `STILL_ACTIVE_ARTIFACTORY_TOKEN`. Authentication expects a `user:password` format for Basic auth, otherwise it will be treated as a bare token for Bearer auth. Valid authentication is required for private JFrog gem registries (`*.jfrog.io`).
|
|
100
|
-
|
|
101
|
-
When providing the artifactory token via flag or env, you must also set `--artifactory-host` or `STILL_ACTIVE_ARTIFACTORY_HOST` to the expected registry hostname (e.g. `my-org.jfrog.io`). still_active only sends the credentials to that host, ensuring that a lockfile containing other Artifactory hosts will not leak the token (a security risk). Providing a token/host in this manner will work only for a single host. To support multiple Artifactory hosts, use Bundler's configuration for per-host credentials.
|
|
102
|
-
|
|
103
|
-
### CLI options
|
|
104
|
-
|
|
105
|
-
```text
|
|
106
|
-
Usage: still_active [options]
|
|
107
|
-
|
|
108
|
-
all flags are optional
|
|
109
|
-
|
|
110
|
-
--gemfile=GEMFILE path to gemfile
|
|
111
|
-
--gems=GEM,GEM2,... Gem(s)
|
|
112
|
-
--sbom=PATH audit a CycloneDX SBOM cross-ecosystem (npm/pypi/cargo/go/maven/nuget); JSON output
|
|
113
|
-
--terminal Coloured terminal output (default in TTY)
|
|
114
|
-
--markdown Markdown table output
|
|
115
|
-
--json JSON output (default when piped)
|
|
116
|
-
--alternatives Suggest maintained alternatives (Ruby Toolbox leads) for archived/critical gems
|
|
117
|
-
--unreleased-commits Count commits on the default branch since the latest release (GitHub only; opt-in)
|
|
118
|
-
--direct-only Audit only direct (declared) deps, not the full transitive graph
|
|
119
|
-
--sarif[=PATH] SARIF 2.1.0 output for GitHub Code Scanning
|
|
120
|
-
--cyclonedx[=PATH] CycloneDX SBOM output (stdout, or a file path)
|
|
121
|
-
--cyclonedx-version=VERSION CycloneDX spec version: 1.6 (default) or 1.7
|
|
122
|
-
--baseline=PATH Compare current state to baseline JSON; emit markdown deltas
|
|
123
|
-
--github-oauth-token=TOKEN GitHub OAuth token to make API calls
|
|
124
|
-
--gitlab-token=TOKEN GitLab personal access token for API calls
|
|
125
|
-
--artifactory-token=TOKEN Artifactory token for private gem registry API calls
|
|
126
|
-
--artifactory-host=HOST Artifactory host allowed to receive the global token (e.g. my-org.jfrog.io)
|
|
127
|
-
--ecosystems-email=EMAIL Contact email to join the ecosyste.ms polite pool (higher rate limit)
|
|
128
|
-
--simultaneous-requests=QTY Number of simultaneous requests made
|
|
129
|
-
--safe-range-end=YEARS maximum years since last release considered safe, no warning (default 1.5)
|
|
130
|
-
--warning-range-end=YEARS maximum years since last release that triggers a warning, beyond this is critical (default 3)
|
|
131
|
-
--fail-if-critical Exit 1 if any gem has critical activity warning
|
|
132
|
-
--fail-if-warning Exit 1 if any gem has warning or critical activity warning
|
|
133
|
-
--fail-if-vulnerable[=SEVERITY]
|
|
134
|
-
Exit 1 if any gem has vulnerabilities (optionally at or above SEVERITY)
|
|
135
|
-
--fail-if-outdated=LIBYEARS Exit 1 if any gem exceeds LIBYEARS behind latest
|
|
136
|
-
--fail-if-poison[=TIER] Exit 1 on a poison-pill at or above TIER (note|warning|critical; default warning)
|
|
137
|
-
--fail-if-language-ceiling[=TIER]
|
|
138
|
-
Exit 1 on a language-runtime ceiling (Ruby/Python; default: EOL-forced only; =note also gates latest-not-yet)
|
|
139
|
-
--ignore=GEM,GEM2,... Exclude gems from pass/fail checks (still shown in output)
|
|
140
|
-
--critical-warning-emoji=EMOJI
|
|
141
|
-
--futurist-emoji=EMOJI
|
|
142
|
-
--success-emoji=EMOJI
|
|
143
|
-
--unsure-emoji=EMOJI
|
|
144
|
-
--warning-emoji=EMOJI
|
|
145
|
-
-h, --help Show this message
|
|
146
|
-
-v, --version Show version
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
### Output formats
|
|
150
|
-
|
|
151
|
-
**Terminal** (default on TTY) -- coloured table with summary line. Shown above.
|
|
152
|
-
|
|
153
|
-
**JSON** (default when piped) -- structured data for automation:
|
|
154
|
-
|
|
155
|
-
```bash
|
|
156
|
-
still_active --json --gemfile=spec/still_active/edge_case_gemfile/Gemfile
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
```json
|
|
160
|
-
{
|
|
161
|
-
"gems": {
|
|
162
|
-
"async": {
|
|
163
|
-
"source_type": "rubygems",
|
|
164
|
-
"version_used": "2.36.0",
|
|
165
|
-
"latest_version": "2.36.0",
|
|
166
|
-
"repository_url": "https://github.com/socketry/async",
|
|
167
|
-
"last_commit_date": "2026-01-22 04:09:48 UTC",
|
|
168
|
-
"archived": false,
|
|
169
|
-
"scorecard_score": 7.1,
|
|
170
|
-
"vulnerability_count": 0,
|
|
171
|
-
"license": "MIT",
|
|
172
|
-
"libyear": 0.0
|
|
173
|
-
},
|
|
174
|
-
"nested_form": {
|
|
175
|
-
"source_type": "git",
|
|
176
|
-
"version_used": "0.3.2",
|
|
177
|
-
"repository_url": "https://github.com/ryanb/nested_form",
|
|
178
|
-
"last_commit_date": "2021-12-11 21:47:02 UTC",
|
|
179
|
-
"archived": true,
|
|
180
|
-
"scorecard_score": 3.3,
|
|
181
|
-
"vulnerability_count": 0
|
|
182
|
-
},
|
|
183
|
-
"local_gem": {
|
|
184
|
-
"source_type": "path",
|
|
185
|
-
"version_used": "0.1.0",
|
|
186
|
-
"scorecard_score": null,
|
|
187
|
-
"vulnerability_count": 0
|
|
188
|
-
}
|
|
189
|
-
},
|
|
190
|
-
"ruby": {
|
|
191
|
-
"version": "4.0.1",
|
|
192
|
-
"eol": false,
|
|
193
|
-
"latest_version": "4.0.1",
|
|
194
|
-
"libyear": 0.0
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
**Markdown** -- table for pull requests, documentation, or wikis:
|
|
200
|
-
|
|
201
|
-
```bash
|
|
64
|
+
# markdown table for a pull request
|
|
202
65
|
still_active --markdown
|
|
203
66
|
```
|
|
204
67
|
|
|
205
|
-
|
|
206
|
-
| -------- | ----------- | ------- | ----- | ------------------------------------------------------------ | -------------------------------------------------------------------------- | -------------------------------------------------------------------------- | ------------------ | ----------------------------------------------------- | ------- | ------- |
|
|
207
|
-
| | ✅ | 7.1/10 | ✅ | [async](https://github.com/socketry/async) | [2.36.0](https://rubygems.org/gems/async/versions/2.36.0) (2026/01) | [2.36.0](https://rubygems.org/gems/async/versions/2.36.0) (2026/01) | ❓ | [2026/01](https://github.com/socketry/async) | 0.0y | MIT |
|
|
208
|
-
| 🚩 | ✅ | 3.6/10 | ✅ | [backbone-rails](https://github.com/aflatter/backbone-rails) | [1.2.3](https://rubygems.org/gems/backbone-rails/versions/1.2.3) (2016/02) | [1.2.3](https://rubygems.org/gems/backbone-rails/versions/1.2.3) (2016/02) | ❓ | [2016/02](https://github.com/aflatter/backbone-rails) | 0.0y | MIT |
|
|
209
|
-
| ❓ | ❓ | ❓ | ✅ | local_gem | 0.1.0 (path) | ❓ | ❓ | ❓ | - | - |
|
|
210
|
-
| 🚩 | ❓ | 3.3/10 | ✅ | [nested_form](https://github.com/ryanb/nested_form) | 0.3.2 (git) | ❓ | ❓ | [2021/12](https://github.com/ryanb/nested_form) | - | MIT |
|
|
68
|
+
Full flags: [`docs/cli.md`](docs/cli.md). Tokens for private registries and self-hosted forges: [`docs/authentication.md`](docs/authentication.md).
|
|
211
69
|
|
|
212
|
-
|
|
70
|
+
## GitHub Action & CI
|
|
213
71
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
```bash
|
|
217
|
-
still_active --cyclonedx # CycloneDX 1.6 to stdout
|
|
218
|
-
still_active --cyclonedx=sbom.json # write to a file
|
|
219
|
-
still_active --cyclonedx --cyclonedx-version=1.7
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
Emits **1.6 by default** — the version mainstream consumers ingest today (`cyclonedx-core-java`/Dependency-Track and `cyclonedx-go`/Trivy both cap at 1.6 as of 2026); `--cyclonedx-version=1.7` opts into the latest. Gem name/version/`purl`/licenses and vulnerabilities map to native CycloneDX fields; maintenance signals with no native home (archived, OpenSSF score, libyear, last commit, yanked) ride in `still_active:`-namespaced `properties`. The `serialNumber` is content-derived, so two SBOMs of the same lockfile differ only by their generation timestamp.
|
|
223
|
-
|
|
224
|
-
### Cross-ecosystem SBOM audit (`--sbom`)
|
|
225
|
-
|
|
226
|
-
still_active is Ruby-first, but the maintenance lens isn't Ruby-only. Point `--sbom` at a CycloneDX SBOM (from [Syft](https://github.com/anchore/syft), Trivy, or any producer) and it assesses `npm`, `pypi`, `cargo`, `go`, `maven`, and `nuget` packages the same way it does gems, sourcing signals from [deps.dev](https://deps.dev) and ecosyste.ms instead of Bundler:
|
|
227
|
-
|
|
228
|
-
```bash
|
|
229
|
-
syft dir:. -o cyclonedx-json > sbom.json
|
|
230
|
-
still_active --sbom=sbom.json # JSON report, keyed "ecosystem/name@version"
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
Each dependency carries the same `activity_level` and lifecycle `status` as the Ruby path, and the `summary` reports the worst status across the SBOM. When the SBOM marks dev-vs-prod scope (CycloneDX `scope`, or a `cyclonedx-npm`-style development property), each dependency also carries a `production` boolean so a consumer can separate prod risk from test debt; generators that don't mark it (Syft drops the label) leave the key absent, which reads as unknown rather than a false all-clear. `--sbom` is mutually exclusive with `--gemfile`/`--gems` and always emits JSON (its shape differs from the Ruby audit, so it omits the `$schema` contract).
|
|
234
|
-
|
|
235
|
-
The SBOM is **untrusted input**: only ecosystem/name/version are read from it, the repository is resolved from deps.dev (never a URL the SBOM supplies, so a hostile SBOM can't redirect a lookup), and anything deps.dev doesn't index degrades to `unknown` rather than a fabricated `ok`. Packages it can't assess (unsupported ecosystems, a component with no version or PURL, or one whose lookup failed against a rate-limited/flaky deps.dev) are surfaced in an `unassessable` list plus a stderr count, never silently dropped, so an audit can't read "all clear" while ignoring what it skipped; a present-but-unreadable SBOM errors rather than reporting a clean empty audit. OS packages land in `unassessable` too: the differentiated play is **maintenance**, not vulnerability scanning, so compose Trivy/Grype for CVEs.
|
|
236
|
-
|
|
237
|
-
The activity (`--fail-if-critical`/`--fail-if-warning`), vulnerability (`--fail-if-vulnerable`), and libyear (`--fail-if-outdated`) gates all apply to SBOM dependencies, which carry `libyear` and `up_to_date` the same as the Ruby path. **The full poison-pill signal is scoped to flat-resolution ecosystems** (rubygems, pypi): there a dormant gem's below-latest cap forces the whole tree onto the old version. npm nests multiple versions and cargo lets majors coexist (and their caret default makes "below latest major" the norm, not a hazard), so the plain below-latest cap there is subtree-local noise and stays suppressed.
|
|
238
|
-
|
|
239
|
-
**The security "below the fix" case, though, does travel to npm and cargo.** When a *dormant or archived* package pins a dependency below the version that patches a HIGH-or-critical advisory, still_active flags it — at patch precision (npm/cargo fixes are mostly same-major patch bumps), and only when *every* resolved copy the constraint governs is vulnerable (a safe copy elsewhere in the tree, or a patched copy the constraint can reach, clears it), so it stays silent unless the tree is genuinely stuck. Example: an archived `request@2.88.2` pinning `form-data ~2.3.2` below the fix for a critical CVE — you can't patch it without replacing `request`. Other Ruby-shaped policy features still don't travel to the SBOM path: `.still_active.yml` suppressions and `--ignore` key on the gem name (SBOM results key on `ecosystem/name@version`).
|
|
240
|
-
|
|
241
|
-
#### Which signal covers which path
|
|
242
|
-
|
|
243
|
-
Most maintenance signals apply everywhere; a few are deliberately scoped (see [`docs/rules.md`](docs/rules.md) for the full rule reference).
|
|
244
|
-
|
|
245
|
-
| Signal (rule) | Ruby (native) | `--sbom` (cross-ecosystem) |
|
|
246
|
-
| ------------- | ------------- | -------------------------- |
|
|
247
|
-
| Archived repo (SA001) | Yes | Yes (npm, PyPI, Cargo, Go, Maven, NuGet) |
|
|
248
|
-
| Abandoned / no recent release (SA002) | Yes | Yes (all of the above) |
|
|
249
|
-
| Low OpenSSF Scorecard (SA005) | Yes | Yes (all of the above) |
|
|
250
|
-
| Libyear drift (SA004) | Yes | Yes (all of the above) |
|
|
251
|
-
| Vulnerabilities (SA003) | Yes (deps.dev + OSV + ruby-advisory-db) | Yes (deps.dev + OSV) |
|
|
252
|
-
| ↳ "below the fix" (dead capper pins a vulnerable dep) | Yes | npm, Cargo, PyPI (patch precision) |
|
|
253
|
-
| Poison-pill / compatibility ceiling (SA008) | Yes (rubygems) | PyPI only (flat resolution; npm/Cargo suppressed) |
|
|
254
|
-
| Language-runtime ceiling (SA009) | Yes (`ruby_version`) | Python only (`requires_python`) |
|
|
255
|
-
| Ruby EOL (SA006) | Yes | n/a (Ruby-runtime signal) |
|
|
256
|
-
| Yanked version (SA007) | Yes | n/a |
|
|
257
|
-
|
|
258
|
-
### SARIF output (GitHub Code Scanning)
|
|
259
|
-
|
|
260
|
-
Emit findings as SARIF 2.1.0 — they show up in the GitHub Security tab and as inline annotations on `Gemfile.lock` in pull requests.
|
|
261
|
-
|
|
262
|
-
> **See it live:** this repo audits itself on every push. Browse the current open findings in the [Code Scanning Security tab](https://github.com/SeanLF/still_active/security/code-scanning?query=tool%3Astill_active+is%3Aopen).
|
|
263
|
-
|
|
264
|
-
```bash
|
|
265
|
-
still_active --sarif # writes still_active.sarif.json
|
|
266
|
-
still_active --sarif=path/to/out.sarif.json
|
|
267
|
-
still_active --sarif=- # stdout
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
**Easy mode** — use the [`still_active-action`](https://github.com/SeanLF/still_active-action) wrapper:
|
|
72
|
+
The [`still_active-action`](https://github.com/SeanLF/still_active-action) uploads findings to your **GitHub Security tab** as SARIF, with inline PR annotations on `Gemfile.lock`:
|
|
271
73
|
|
|
272
74
|
```yaml
|
|
273
75
|
permissions:
|
|
@@ -290,216 +92,114 @@ jobs:
|
|
|
290
92
|
with: { sarif_file: still_active.sarif.json }
|
|
291
93
|
```
|
|
292
94
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
```yaml
|
|
296
|
-
- run: bundle exec still_active --sarif
|
|
297
|
-
env:
|
|
298
|
-
GITHUB_TOKEN: ${{ github.token }}
|
|
299
|
-
- uses: github/codeql-action/upload-sarif@v3
|
|
300
|
-
if: always()
|
|
301
|
-
with: { sarif_file: still_active.sarif.json }
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
Rule reference (SA001–SA009) and how to suppress: see [`docs/rules.md`](docs/rules.md).
|
|
95
|
+
Gate the build on what you care about (all off by default, so nothing breaks until you opt in), and use `--baseline` for PR review.
|
|
305
96
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
`--baseline=FILE` compares the current run against a previously captured JSON snapshot and emits a markdown delta report. Designed for the PR question reviewers actually ask: **what got worse?**
|
|
97
|
+
<details>
|
|
98
|
+
<summary>CI gate flags and PR-review diff</summary>
|
|
309
99
|
|
|
310
100
|
```bash
|
|
311
|
-
#
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
101
|
+
still_active --fail-if-critical # upstream critically stale or archived
|
|
102
|
+
still_active --fail-if-vulnerable # any known vulnerability (or =low|medium|high|critical)
|
|
103
|
+
still_active --fail-if-outdated=3 # more than 3 libyears behind latest
|
|
104
|
+
still_active --fail-if-poison # a dormant package caps a dep below its latest major
|
|
105
|
+
still_active --fail-if-language-ceiling # a pin strands you on an EOL language runtime
|
|
315
106
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
107
|
+
# PR review: report only what got worse since a saved snapshot, exit 1 on any regression
|
|
108
|
+
still_active --json > /tmp/main.json && still_active --baseline=/tmp/main.json
|
|
109
|
+
```
|
|
110
|
+
</details>
|
|
319
111
|
|
|
320
|
-
|
|
112
|
+
Rule reference (SA001-SA009), suppression, and composing with `dependency-review-action`: [`docs/rules.md`](docs/rules.md), [`docs/ci.md`](docs/ci.md). This repo audits itself every push, so you can browse live findings in its [Code Scanning tab](https://github.com/SeanLF/still_active/security/code-scanning?query=tool%3Astill_active+is%3Aopen).
|
|
321
113
|
|
|
322
|
-
|
|
114
|
+
## Cross-ecosystem audit
|
|
323
115
|
|
|
324
|
-
|
|
116
|
+
Point `--sbom` at a CycloneDX SBOM (from [Syft](https://github.com/anchore/syft), Trivy, or any producer) and `still_active` assesses `npm`, `pypi`, `cargo`, `go`, `maven`, and `nuget` packages the same way it does gems, via [deps.dev](https://deps.dev) and [ecosyste.ms](https://ecosyste.ms). The SBOM is treated as **untrusted input** (only ecosystem/name/version are read, repositories resolve from deps.dev, and anything unassessable is surfaced rather than faked as `ok`). Most signals apply everywhere; a few are deliberately scoped.
|
|
325
117
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
| Platform | GitHub.com / GHES only | Any CI |
|
|
329
|
-
| Languages | Multi (GitHub dep graph) | Ruby |
|
|
330
|
-
| Vulnerabilities | GHSA | deps.dev + ruby-advisory-db (merged) |
|
|
331
|
-
| Licenses | Yes (allow/deny gating) | Surfaced (no gating) |
|
|
332
|
-
| OpenSSF Scorecard | Yes (display) | Yes (display + threshold) |
|
|
333
|
-
| **Last-commit activity** | - | **Yes** |
|
|
334
|
-
| **Archived repo detection** | - | **Yes** |
|
|
335
|
-
| **Libyear drift** | - | **Yes** |
|
|
336
|
-
| **Ruby EOL detection** | - | **Yes** |
|
|
337
|
-
| **Yanked version detection** | - | **Yes** |
|
|
338
|
-
| Diff vs base | Native (GitHub API) | `--baseline=FILE` |
|
|
339
|
-
| Output | Inline PR annotations | Terminal / Markdown / JSON / SARIF / CycloneDX |
|
|
118
|
+
<details>
|
|
119
|
+
<summary>Which signal covers which ecosystem</summary>
|
|
340
120
|
|
|
341
|
-
|
|
121
|
+
| Signal (rule) | Ruby (native) | `--sbom` (cross-ecosystem) |
|
|
122
|
+
| ------------- | ------------- | -------------------------- |
|
|
123
|
+
| Archived (SA001), no recent release (SA002), low OpenSSF (SA005), libyear (SA004) | Yes | Yes (all six ecosystems) |
|
|
124
|
+
| Vulnerabilities (SA003) | deps.dev + OSV + ruby-advisory-db | deps.dev + OSV |
|
|
125
|
+
| ↳ "below the fix" (a dead package pins a vulnerable dep) | Yes | npm, Cargo, PyPI |
|
|
126
|
+
| Poison-pill (SA008) | rubygems | PyPI (flat resolution only) |
|
|
127
|
+
| Language-runtime ceiling (SA009) | Ruby | Python |
|
|
128
|
+
| Ruby EOL (SA006), yanked (SA007) | Yes | n/a |
|
|
342
129
|
|
|
343
|
-
|
|
344
|
-
|
|
130
|
+
Full rule detail in [`docs/rules.md`](docs/rules.md).
|
|
131
|
+
</details>
|
|
345
132
|
|
|
346
|
-
|
|
347
|
-
dependency-review:
|
|
348
|
-
runs-on: ubuntu-latest
|
|
349
|
-
steps:
|
|
350
|
-
- uses: actions/checkout@v4
|
|
351
|
-
- uses: actions/dependency-review-action@v4
|
|
352
|
-
with:
|
|
353
|
-
fail-on-severity: high
|
|
354
|
-
show-openssf-scorecard: true
|
|
133
|
+
The play is **maintenance**, not CVE scanning, so compose Trivy/Grype for full vulnerability coverage.
|
|
355
134
|
|
|
356
|
-
|
|
357
|
-
runs-on: ubuntu-latest
|
|
358
|
-
steps:
|
|
359
|
-
- uses: actions/checkout@v4
|
|
360
|
-
- uses: ruby/setup-ruby@v1
|
|
361
|
-
with: { ruby-version: ".ruby-version", bundler-cache: true }
|
|
362
|
-
- uses: SeanLF/still_active-action@v0
|
|
363
|
-
with:
|
|
364
|
-
fail-if-critical: true
|
|
365
|
-
```
|
|
135
|
+
## Output formats
|
|
366
136
|
|
|
367
|
-
|
|
137
|
+
Auto-detected: a coloured terminal table on a TTY (above), JSON when piped. Or ask explicitly.
|
|
368
138
|
|
|
369
|
-
|
|
139
|
+
<details>
|
|
140
|
+
<summary><strong>JSON</strong> (<code>--json</code>): a versioned, contract-tested schema for automation</summary>
|
|
370
141
|
|
|
371
|
-
```
|
|
372
|
-
|
|
373
|
-
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"gems": {
|
|
145
|
+
"nested_form": {
|
|
146
|
+
"source_type": "git",
|
|
147
|
+
"version_used": "0.3.2",
|
|
148
|
+
"repository_url": "https://github.com/ryanb/nested_form",
|
|
149
|
+
"archived": true,
|
|
150
|
+
"scorecard_score": 3.3,
|
|
151
|
+
"vulnerability_count": 0,
|
|
152
|
+
"status": "archived"
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
"ruby": { "version": "4.0.5", "eol": false, "latest_version": "4.0.5" }
|
|
156
|
+
}
|
|
157
|
+
```
|
|
374
158
|
|
|
375
|
-
|
|
376
|
-
|
|
159
|
+
Fields are documented in [`docs/schema.md`](docs/schema.md).
|
|
160
|
+
</details>
|
|
377
161
|
|
|
378
|
-
|
|
379
|
-
|
|
162
|
+
<details>
|
|
163
|
+
<summary><strong>Markdown</strong> (<code>--markdown</code>): a table for pull requests, docs, or wikis</summary>
|
|
380
164
|
|
|
381
|
-
|
|
382
|
-
|
|
165
|
+
| activity | up to date? | OpenSSF | vulns | name | version | last commit |
|
|
166
|
+
| -------- | ----------- | ------- | ----- | ---- | ------- | ----------- |
|
|
167
|
+
| | ✅ | 5.9/10 | ✅ | [nokogiri](https://github.com/sparklemotion/nokogiri) | 1.19.4 | 2026/06 |
|
|
168
|
+
| 🚩 | ✅ | 2/10 | ✅ | [paperclip](https://github.com/thoughtbot/paperclip) | 6.1.0 | 2021/03 |
|
|
169
|
+
</details>
|
|
383
170
|
|
|
384
|
-
#
|
|
385
|
-
still_active --fail-if-outdated=3 --json
|
|
171
|
+
**SARIF** feeds GitHub Code Scanning (see [GitHub Action & CI](#github-action--ci)). **CycloneDX** (`--cyclonedx`) emits a standards-track SBOM so your graph and `still_active`'s signals flow into Trivy, Dependency-Track, or Snyk.
|
|
386
172
|
|
|
387
|
-
|
|
388
|
-
still_active --fail-if-warning --fail-if-vulnerable --ignore=legacy_gem --json
|
|
389
|
-
```
|
|
173
|
+
## Configuration
|
|
390
174
|
|
|
391
|
-
|
|
175
|
+
A committed `.still_active.yml` keeps policy in version control and replaces the blunt `--ignore=GEM` (which mutes *every* gate for a gem) with granular, expiring suppression. A vulnerability suppression must name an advisory id (so a new CVE is never pre-silenced), an `expires:` date makes accepted risk re-surface instead of rotting, and a suppressed finding still shows in output (and as a dismissed SARIF entry).
|
|
392
176
|
|
|
393
|
-
|
|
177
|
+
<details>
|
|
178
|
+
<summary><code>.still_active.yml</code> example</summary>
|
|
394
179
|
|
|
395
180
|
```yaml
|
|
396
|
-
#
|
|
397
|
-
|
|
398
|
-
fail_if_vulnerable: high # true, or a minimum severity: low|medium|high|critical
|
|
399
|
-
fail_if_outdated: 3 # libyears
|
|
400
|
-
fail_if_poison: warning # true (=warning), or a tier: note|warning|critical (severity scales with majors-behind)
|
|
401
|
-
fail_if_language_ceiling: true # true = EOL-forced ceilings only; :note also gates latest-not-yet (fluctuates with the runtime's release calendar)
|
|
402
|
-
unreleased_commits: true
|
|
403
|
-
output: json # terminal | markdown | json
|
|
404
|
-
direct_only: true # audit only declared deps, not the full transitive graph (--direct-only)
|
|
405
|
-
|
|
406
|
-
# Pull bundler-audit's accepted-advisory list instead of maintaining two files
|
|
407
|
-
import:
|
|
408
|
-
- .bundler-audit.yml
|
|
181
|
+
fail_if_vulnerable: high # true, or a minimum severity: low|medium|high|critical
|
|
182
|
+
fail_if_poison: warning # true (=warning), or a tier: note|warning|critical
|
|
409
183
|
|
|
410
184
|
ignore:
|
|
411
|
-
#
|
|
185
|
+
# accept ONE advisory by id; a different or new CVE on nokogiri still fails
|
|
412
186
|
- advisory: CVE-2024-1234
|
|
413
187
|
gem: nokogiri
|
|
414
188
|
reason: "no fix released; not reachable from our code path"
|
|
415
|
-
expires: 2026-09-01
|
|
416
|
-
|
|
417
|
-
# Accept staleness on a vendored gem, but still fail if it gets a CVE
|
|
418
|
-
- gem: legacy_thing
|
|
419
|
-
signal: activity # activity | vulnerability | libyear | poison | language_ceiling
|
|
420
|
-
reason: "vendored, intentionally frozen"
|
|
421
|
-
|
|
422
|
-
# A bare gem name keeps the old whole-gem behaviour (mutes every signal)
|
|
423
|
-
- some_internal_gem
|
|
424
|
-
```
|
|
425
|
-
|
|
426
|
-
Design:
|
|
427
|
-
|
|
428
|
-
- **Granularity.** Key by `advisory:` (one CVE) and/or `signal:` (`activity` / `vulnerability` / `libyear` / `poison`), not the whole gem. A vulnerability suppression **must** name an advisory id, so a newly disclosed CVE on the same gem is never pre-silenced. `--ignore=GEM` (and a bare gem-name entry) still mute everything, by design.
|
|
429
|
-
- **Precedence.** CLI flag > env var > config file > default. A CLI flag always wins; `--ignore` unions with the file's suppressions rather than replacing them. Secrets (tokens) and invocation-specific paths (`--gemfile`, `--gems`, `--baseline`, output paths) are intentionally **not** read from the file, so a committed config never carries a credential.
|
|
430
|
-
- **Expiry.** An `expires:` date makes accepted risk visible: once it passes, the entry stops applying and the finding fails the gate again (Trivy-style), so a suppression can't rot silently. `reason:` is optional but recommended. A suppression naming a gem that isn't in your dependency graph (a typo, or a gem you've since removed) is also surfaced as a warning, so dead entries don't accumulate.
|
|
431
|
-
- **bundler-audit, suggested not absorbed.** still_active never silently inherits another tool's ignore list: auto-importing `.bundler-audit.yml` would suppress vulnerabilities you only accepted in bundler-audit's context, with no reason or expiry here. Instead, when `--fail-if-vulnerable` is on and an un-imported `.bundler-audit.yml` is present, it prints a one-line hint suggesting the `import:` line, leaving the opt-in to you.
|
|
432
|
-
- **Output.** Suppression changes the **exit code** and marks the finding in **SARIF** as a native `suppressions[]` entry (with your `reason` as the justification, so GitHub Code Scanning renders it dismissed rather than open). The finding still appears in JSON/terminal/markdown output; suppression accepts a risk, it doesn't hide that the risk exists.
|
|
433
|
-
|
|
434
|
-
### Activity thresholds
|
|
435
|
-
|
|
436
|
-
Activity is driven by release recency (the latest stable or pre-release date), since a release is what you can actually `bundle update` to. A recent commit does not offset a stale release: the last commit date is shown as context and only stands in when a gem has no releases at all (e.g. git-sourced). Thresholds are calibrated against real RubyGems cadence, where healthy mature gems often go a year or more between releases:
|
|
437
|
-
|
|
438
|
-
- **ok**: last release within 18 months (configurable with `--safe-range-end`)
|
|
439
|
-
- **stale**: last release between 18 months and 3 years ago
|
|
440
|
-
- **critical**: last release over 3 years ago (configurable with `--warning-range-end`)
|
|
441
|
-
|
|
442
|
-
### Alternative gem leads (opt-in)
|
|
443
|
-
|
|
444
|
-
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:
|
|
445
|
-
|
|
446
|
-
```bash
|
|
447
|
-
still_active --gems=paperclip --alternatives
|
|
189
|
+
expires: 2026-09-01 # re-surfaces as a normal failure after this date
|
|
448
190
|
```
|
|
191
|
+
</details>
|
|
449
192
|
|
|
450
|
-
|
|
451
|
-
↳ leads (Ruby Toolbox): shrine · carrierwave · kt-paperclip (verify fit)
|
|
452
|
-
```
|
|
193
|
+
Full semantics, thresholds, and transitive behaviour: [`docs/configuration.md`](docs/configuration.md).
|
|
453
194
|
|
|
454
|
-
|
|
195
|
+
## Data sources
|
|
455
196
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
Maintenance signals (stale releases, archived repos, last-commit age, advisories, libyear) cover the **full transitive lockfile graph by default** — an unmaintained gem you ship transitively is real risk even though you never named it, and the CVE tools still_active composes with already enumerate the whole resolved graph. This matches libyear-bundler (transitive by default, `--only-explicit` opts out) and every CVE scanner (bundler-audit, npm/cargo/pip-audit are all full-tree).
|
|
459
|
-
|
|
460
|
-
When a transitive gem trips a signal, the output names the **direct dependency that pulls it in**: `dependency_path` in JSON, a dimmed `↳ transitive, pulled in by X` line in the terminal, a `(transitive, pulled in by X)` suffix in SARIF messages, and a **Transitive findings** list in markdown. That turns an un-actionable transitive finding into an actionable one: you can't bump a gem you didn't choose, but you can replace or pressure the direct gem that drags it in.
|
|
461
|
-
|
|
462
|
-
`--alternatives` stays **direct-only** by design — "replace gem X with Y" is incoherent for a gem you never selected. Pass `--direct-only` to audit just your declared dependencies (the pre-1.7 behaviour), which is also much cheaper in API calls.
|
|
463
|
-
|
|
464
|
-
> **Run on a schedule, not on every commit.** Auditing the full graph means a repo/release/advisory lookup for *every* resolved gem (hundreds, for a real app), and the GitHub signals are rate-limited (60 req/hour unauthenticated, 5000 with a token). Maintenance status changes over days and weeks, not per-commit, so a nightly or weekly job (or `--direct-only` in PR gates) gets you the signal without burning your API budget for nothing. This is why advisory tools like Brakeman ship their check database; still_active's signals are inherently live (a release date or an archived flag can't be vendored), so the answer is cadence, not bundling.
|
|
465
|
-
|
|
466
|
-
### Unreleased commits (opt-in)
|
|
467
|
-
|
|
468
|
-
`--unreleased-commits` adds an `unreleased_commits` count to the JSON output: commits on the default branch since the latest release's tag. It catches the case the release-recency signal can't, a gem with a *recent* release but a pile of merged-but-unreleased fixes sitting on top, or conversely one that looks stale but is genuinely *done* (no unreleased work). It is the one maintenance signal no Ruby tool surfaces today; only GitHub's own UI shows it.
|
|
469
|
-
|
|
470
|
-
It is **opt-in and GitHub-only**: enabling it adds one extra API call per GitHub-hosted gem (the git tag is resolved from the RubyGems version by trying `v1.2.3` then `1.2.3` as the compare base), so mind your rate limit on a large lockfile. Non-GitHub sources report `null` (GitLab has no equivalent scalar; the signal is duck-typed, so a provider either implements it or doesn't). The count is **informational and never gates a run**. Read it as a lead, not a verdict: it is inflated for monorepos (the count spans the whole repository, e.g. `bundler` living in `rubygems/rubygems`) and for release-branch projects (the default branch is the next-version trunk, so `rails` reads ~2000 commits ahead of its latest stable tag).
|
|
471
|
-
|
|
472
|
-
### Data sources
|
|
473
|
-
|
|
474
|
-
- **Versions, release dates, and licenses** from [RubyGems.org](https://rubygems.org), [GitHub Packages](https://docs.github.com/en/packages), or [JFrog Artifactory](https://jfrog.com/artifactory/) gem registries
|
|
475
|
-
- **Last commit date and archived status** from the [GitHub](https://docs.github.com/en/rest), [GitLab](https://docs.gitlab.com/ee/api/), or [Forgejo/Gitea](https://forgejo.org/docs/latest/user/api-usage/) (Codeberg) API — or, for GitHub repos when no token is configured, from [ecosyste.ms](https://ecosyste.ms) ([repos API](https://repos.ecosyste.ms)). ecosyste.ms data is licensed [CC-BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/).
|
|
476
|
-
- **OpenSSF Scorecard**, **vulnerability counts**, and **CVSS severity** from Google's [deps.dev](https://deps.dev) API (also the cross-ecosystem source on the `--sbom` path)
|
|
477
|
-
- **Advisory severity labels and fixed-version ranges** from [OSV](https://osv.dev) (`api.osv.dev`), which drive the "below the fix" signal. An advisory that publishes only a CVSS **4.0** vector stays unscored unless you install the optional [`cvss-suite`](https://rubygems.org/gems/cvss-suite) gem (`gem install cvss-suite`), which computes its 4.0 base score; without it the gate still fires on the OSV/GHSA severity label, it just skips the number.
|
|
478
|
-
- **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)
|
|
479
|
-
- **Ruby version freshness** from [endoflife.date](https://endoflife.date)
|
|
480
|
-
- **Alternative gem leads** (with `--alternatives`) from the [rubytoolbox/catalog](https://github.com/rubytoolbox/catalog) category data
|
|
481
|
-
|
|
482
|
-
### Configuration defaults
|
|
483
|
-
|
|
484
|
-
| Option | Default | Description |
|
|
485
|
-
| ----------------------- | ----------- | ---------------------------------------------------------------- |
|
|
486
|
-
| `output_format` | auto-detect | Coloured terminal on TTY, JSON when piped |
|
|
487
|
-
| `safe_range_end` | 1.5 years | Last release within this range is "ok" |
|
|
488
|
-
| `warning_range_end` | 3 years | Last release within this range is "stale"; beyond is "critical" |
|
|
489
|
-
| `simultaneous_requests` | 10 | Concurrent API requests |
|
|
197
|
+
Release dates and licenses from [RubyGems](https://rubygems.org) / [GitHub Packages](https://docs.github.com/en/packages) / [Artifactory](https://jfrog.com/artifactory/); repo activity from the [GitHub](https://docs.github.com/en/rest) / [GitLab](https://docs.gitlab.com/ee/api/) / [Codeberg](https://forgejo.org/docs/latest/user/api-usage/) API, or [ecosyste.ms](https://ecosyste.ms) tokenless ([CC-BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)); OpenSSF Scorecard and CVSS from [deps.dev](https://deps.dev); advisory severities and fixed-version ranges from [OSV](https://osv.dev) (CVSS-4 scoring needs the optional [`cvss-suite`](https://rubygems.org/gems/cvss-suite)); extra advisories from [ruby-advisory-db](https://github.com/rubysec/ruby-advisory-db); runtime EOL from [endoflife.date](https://endoflife.date).
|
|
490
198
|
|
|
491
199
|
## Development
|
|
492
200
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
A pre-push hook runs `rake` automatically before each `git push`, so cross-file rubocop rules don't escape to CI. Skip with `git push --no-verify` if you really need to.
|
|
496
|
-
|
|
497
|
-
To install this gem onto your local machine, run `bundle exec rake install`. New versions are published automatically to [rubygems.org](https://rubygems.org) when a GitHub Release is created (via trusted publishing).
|
|
498
|
-
|
|
499
|
-
## Contributing
|
|
500
|
-
|
|
501
|
-
Bug reports and pull requests are welcome.
|
|
201
|
+
`bin/setup` installs dependencies and wires git hooks; `rake` runs the full lint + test suite. A pre-push hook runs `rake` automatically. Releases publish to [rubygems.org](https://rubygems.org) automatically on a GitHub Release (trusted publishing). See [`CONTRIBUTING.md`](CONTRIBUTING.md).
|
|
502
202
|
|
|
503
203
|
## License
|
|
504
204
|
|
|
505
|
-
|
|
205
|
+
Open source under the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -22,6 +22,24 @@ module StillActive
|
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
# The latest pre-release is only a useful signal when it is newer than the
|
|
26
|
+
# latest stable release: an upcoming version you could opt into. A pre-release
|
|
27
|
+
# that predates the latest stable is historical noise (e.g. a lone 2009 `rc`
|
|
28
|
+
# on a gem now at 0.9.x, or an `8.1.0.rc1` after `8.1.2` already shipped), and
|
|
29
|
+
# it silently corrupts downstream logic: up_to_date compares `used >=`
|
|
30
|
+
# latest_pre_release, so any current version reads `>=` a stale pre-release and
|
|
31
|
+
# a behind gem is painted up to date (the futurist emoji). Returns the
|
|
32
|
+
# pre-release only when strictly newer than the release; keeps it when there is
|
|
33
|
+
# no stable release at all (a pre-release-only gem), where it is the only signal.
|
|
34
|
+
def upcoming_pre_release(pre_release:, release:)
|
|
35
|
+
return pre_release if release.nil?
|
|
36
|
+
return if pre_release.nil?
|
|
37
|
+
|
|
38
|
+
pre = to_gem_version(pre_release)
|
|
39
|
+
stable = to_gem_version(release)
|
|
40
|
+
pre_release if pre && stable && pre > stable
|
|
41
|
+
end
|
|
42
|
+
|
|
25
43
|
def up_to_date(version_used:, latest_version: nil, latest_pre_release_version: nil)
|
|
26
44
|
return if latest_version.nil? && latest_pre_release_version.nil?
|
|
27
45
|
|
data/lib/still_active/version.rb
CHANGED
|
@@ -153,7 +153,10 @@ module StillActive
|
|
|
153
153
|
commit_date = signals[:last_commit_date]
|
|
154
154
|
archived = signals[:archived]
|
|
155
155
|
last_release = VersionHelper.find_version(versions: vs, pre_release: false)
|
|
156
|
-
last_pre_release = VersionHelper.
|
|
156
|
+
last_pre_release = VersionHelper.upcoming_pre_release(
|
|
157
|
+
pre_release: VersionHelper.find_version(versions: vs, pre_release: true),
|
|
158
|
+
release: last_release,
|
|
159
|
+
)
|
|
157
160
|
deps_dev = fetch_deps_dev_info(
|
|
158
161
|
gem_name: gem_name,
|
|
159
162
|
version: gem_version || VersionHelper.gem_version(version_hash: last_release),
|