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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b3541d297e3d8059ffdaee4c1377ab7333fc5d2a54940219d7a4f2611f4c445e
4
- data.tar.gz: d121da3f25a1db5bbcf5cc3e2351acbaef84076350bccc3074ed869d46881f75
3
+ metadata.gz: 89af276523def67c4566cdcc8ff084c14383f4c43dff3429a739f861cfea8555
4
+ data.tar.gz: 8a6da7245242d2778126ed993484fd5bddf0cfba66d612cc120c705c1c519383
5
5
  SHA512:
6
- metadata.gz: e6f9391ed541b846f1c65cd0231e6894c69606cfbce1f6c9e636926607e99191c7029f7c79d5fe42257bbf33bb822f84b7772903c3444ce0b593ae8c57c875d6
7
- data.tar.gz: 15b01401efb0f22e189c16cd5baf5f3086a2969686e3efd701c85c34ef7dc29eba1ede2eecb717f59e10b6d827d8ae729be07a6c42261583f8256dd7c060a7ab
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 if your Ruby dependencies are still maintained?**
3
+ **How do you know the dependencies you ship 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, vulnerabilities (flagging those with **no fixed version available**, the ones you can't upgrade out of), libyear drift, and archived repos for every gem in your dependency graph, direct and transitive by default (`--direct-only` to narrow), with granular, committed suppression via `.still_active.yml`.
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
- Ruby-first, but the maintenance lens isn't Ruby-only: point [`--sbom`](#cross-ecosystem-sbom-audit---sbom) at a CycloneDX SBOM and `still_active` audits your **npm, PyPI, Cargo, Go, Maven, and NuGet** dependencies the same way it does gems.
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
- Findings ship as **terminal / markdown / JSON / SARIF / CycloneDX** SARIF lands in your GitHub Security tab and as inline PR annotations on `Gemfile.lock`; CycloneDX feeds Trivy / Dependency-Track / Snyk. PR mode (`--baseline=FILE`) reports only what got worse since main, so reviewers see one line ("`vcr` newly archived") instead of an absolute snapshot of every dep.
10
-
11
- [![Gem Version](https://badge.fury.io/rb/still_active.svg)](https://badge.fury.io/rb/still_active)
12
- [![GitHub Action](https://img.shields.io/badge/Marketplace-still__active--action-2ea44f?logo=github)](https://github.com/marketplace/actions/still_active)
13
- ![Code Quality analysis](https://github.com/SeanLF/still_active/actions/workflows/codeql-analysis.yml/badge.svg)
14
- ![RSpec](https://github.com/SeanLF/still_active/actions/workflows/rspec.yml/badge.svg)
15
- ![Rubocop analysis](https://github.com/SeanLF/still_active/actions/workflows/rubocop-analysis.yml/badge.svg)
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 Version Activity OpenSSF Vulns License
19
- ──────────────────────────────────────────────────────────────────────────────
20
- async 2.36.0 (latest) ok 7.1/10 0 MIT
21
- backbone-rails 1.2.3 (latest) archived 3.6/10 0 MIT
22
- bootstrap-slider-rails 9.8.0 (latest) critical - 0 MIT
23
- gitlab-markup 2.0.0 (latest) ok - 0 MIT
24
- local_gem 0.1.0 (path) - - 0 -
25
- nested_form 0.3.2 (git) archived 3.3/10 0 MIT
26
- remotipart 1.4.4 (git) critical 3.1/10 0 MIT
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
- `still_active` is **complementary to** -- not a replacement for -- the established Ruby tooling. `bundle outdated`, `bundler-audit`, and `libyear-bundler` are purpose-built and battle-tested at what they do. `still_active` answers a different question: **is anyone still maintaining this gem?** -- and folds in the version/CVE/libyear signals so you get one report instead of three.
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 commit activity** | - | - | - | **Yes** |
35
+ | **Last-commit activity** | - | - | - | **Yes** |
42
36
  | **Archived repo detection** | - | - | - | **Yes** |
43
37
  | **OpenSSF Scorecard** | - | - | - | **Yes** |
44
- | **Yanked version detection** | - | - | - | **Yes** |
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
- | GitLab support | - | - | - | Yes |
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
- The bolded rows are the gap `still_active` fills: nobody else answers "is the maintainer still around?" The CVE column is worth a closer look: `bundler-audit` reads `ruby-advisory-db` and `still_active` reads `deps.dev`, which sometimes diverge. **If `bundler-audit` is installed alongside `still_active`, we read its `ruby-advisory-db` checkout too and merge the results** (deduplicated, each advisory tagged with its `source`) — so running both no longer means reconciling two different vuln counts by hand.
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
- **Requires an actively-maintained Ruby.** The gemspec's `required_ruby_version` floor tracks Ruby's [EOL schedule](https://endoflife.date/ruby); running a maintenance auditor on an unmaintained runtime would be a bit rich. You don't have to run it *on* the Ruby you're auditing, though: still_active reports on the version your project pins in `Gemfile.lock`, so run it from any current Ruby (locally, in CI, or via the [`still_active-action`](https://github.com/SeanLF/still_active-action)) and it will still flag an EOL target.
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 Start
52
+ ## Quick start
62
53
 
63
54
  ```bash
64
55
  # audit your Gemfile (auto-detects output format)
65
56
  still_active
66
57
 
67
- # check specific gems
68
- still_active --gems=rails,nokogiri,sidekiq
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 pipeline: fail if any gem is critically stale or has vulnerabilities
61
+ # fail CI if any gem is critically stale or vulnerable
71
62
  still_active --fail-if-critical --fail-if-vulnerable
72
63
 
73
- # ignore specific gems in CI checks
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
- | activity | up to date? | OpenSSF | vulns | name | version used | latest version | latest pre-release | last commit | libyear | license |
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
- **Ruby 4.0.1** (latest)
70
+ ## GitHub Action & CI
213
71
 
214
- **CycloneDX** -- a standards-track SBOM so your dependency graph and still_active's signals flow into Trivy, Dependency-Track, or Snyk:
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
- | &nbsp;&nbsp;↳ "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
- **Plain bundle exec** if you'd rather pin still_active in your Gemfile:
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
- ### Baseline diff (PR review)
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
- # Locally capture from main, compare to your branch
312
- git checkout main && still_active --json > /tmp/main.json
313
- git checkout my-branch && still_active --baseline=/tmp/main.json
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
- In CI, capture a baseline on main and compare on PR branches. Exits 1 if any regression is detected (new vulns, newly-archived deps, scorecard drops crossing 7.0, libyear growth on unchanged versions, Ruby newly EOL, etc.).
317
-
318
- The diff supersedes `--sarif`, `--terminal`, `--markdown`, and `--json` when set.
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
- When a run is detected as Dependabot- or Renovate-authored (via `GITHUB_ACTOR`, a `dependabot/`/`renovate/` branch, or the commit subject), the report leads with a one-line narrative — "Dependabot bump: `rack` 2.0.0 → 2.0.6" and `--json` gains a top-level `pr_context`. Detection is best-effort and conservative: it never produces a false positive on an ordinary commit, and a miss costs only the narrative line.
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
- ### Alongside `dependency-review-action`
114
+ ## Cross-ecosystem audit
323
115
 
324
- GitHub's first-party [`dependency-review-action`](https://github.com/actions/dependency-review-action) runs server-side on PRs and surfaces **vulnerabilities, licenses, and OpenSSF Scorecard** scores from GitHub's dependency-graph diff. It does not surface maintenance signals last-commit activity, archived repos, libyear, Ruby EOL, or yanked versions and is GitHub.com / GHES only. `still_active` is the complement, not a replacement:
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
- | | `dependency-review-action` | `still_active` |
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
- Run both: let `dependency-review-action` gate CVEs and licenses, and `still_active` add the maintenance lens on the same PR.
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
+ | &nbsp;&nbsp;↳ "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
- ```yaml
344
- on: pull_request
130
+ Full rule detail in [`docs/rules.md`](docs/rules.md).
131
+ </details>
345
132
 
346
- jobs:
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
- maintenance-review:
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
- ### CI quality gating
137
+ Auto-detected: a coloured terminal table on a TTY (above), JSON when piped. Or ask explicitly.
368
138
 
369
- Use exit-code flags to fail CI pipelines based on dependency status:
139
+ <details>
140
+ <summary><strong>JSON</strong> (<code>--json</code>): a versioned, contract-tested schema for automation</summary>
370
141
 
371
- ```bash
372
- # fail on critically stale or archived gems
373
- still_active --fail-if-critical --json
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
- # fail on any stale, critical, or archived gem
376
- still_active --fail-if-warning --json
159
+ Fields are documented in [`docs/schema.md`](docs/schema.md).
160
+ </details>
377
161
 
378
- # fail if any gem has known vulnerabilities
379
- still_active --fail-if-vulnerable --json
162
+ <details>
163
+ <summary><strong>Markdown</strong> (<code>--markdown</code>): a table for pull requests, docs, or wikis</summary>
380
164
 
381
- # fail only on high/critical severity vulnerabilities
382
- still_active --fail-if-vulnerable=high --json
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
- # fail if any gem is more than 3 libyears behind
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
- # combine flags and exclude known exceptions
388
- still_active --fail-if-warning --fail-if-vulnerable --ignore=legacy_gem --json
389
- ```
173
+ ## Configuration
390
174
 
391
- ### Configuration file (`.still_active.yml`)
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
- `--ignore=GEM` is blunt: it drops a gem from **every** gate at once, so accepting one unfixable advisory also blinds you to that gem going archived or to a *new* CVE. A committed `.still_active.yml` in the project root replaces that with granular, auditable suppression, and lets you keep your policy flags in version control instead of threading them through every invocation.
177
+ <details>
178
+ <summary><code>.still_active.yml</code> example</summary>
394
179
 
395
180
  ```yaml
396
- # .still_active.yml -- policy defaults plus granular suppression
397
- fail_if_critical: true
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
- # Accept ONE advisory, by id -- a different/new CVE on nokogiri still fails
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 # re-surfaces as a normal failure after this date
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
- ```text
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
- These are **leads, not recommendations**: same-category does not mean drop-in replacement, so verify fit before switching. Ruby has no authoritative "use instead" metadata (unlike npm `deprecate`, Go's `// Deprecated:`, or NuGet's alternate-package field), so this is a best-effort heuristic. It is silent when the catalog has no entry for the gem, and the feature never blocks or fails a run. Leads appear in terminal, markdown, JSON, and SARIF output. When the flag is off, terminal output shows a one-line hint on flagged gems that the option exists (other formats stay silent).
195
+ ## Data sources
455
196
 
456
- ### Transitive dependencies
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
- After checking out the repo, run `bin/setup` to install dependencies and wire git hooks. Then run `rake` to run the full lint + test suite (`rake spec` for just tests, `rake rubocop` for just lint). You can also run `bin/console` for an interactive prompt that will allow you to experiment.
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
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module StillActive
4
- VERSION = "3.0.0.rc1"
4
+ VERSION = "3.0.0.rc2"
5
5
  end
@@ -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.find_version(versions: vs, pre_release: true)
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),
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: still_active
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0.rc1
4
+ version: 3.0.0.rc2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sean Floyd