still_active
How do you know if your Ruby dependencies are still maintained?
bundle outdated tells you version drift. bundler-audit catches known CVEs. Neither tells you whether anyone is still working on the thing. still_active checks maintenance activity, version freshness, security scores, vulnerabilities, libyear drift, and archived repos for every gem in your Gemfile.
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.
Name Version Activity OpenSSF Vulns License
──────────────────────────────────────────────────────────────────────────────
async 2.36.0 (latest) ok 7.1/10 0 MIT
backbone-rails 1.2.3 (latest) archived 3.6/10 0 MIT
bootstrap-slider-rails 9.8.0 (latest) critical - 0 MIT
gitlab-markup 2.0.0 (latest) ok - 0 MIT
local_gem 0.1.0 (path) - - 0 -
nested_form 0.3.2 (git) archived 3.3/10 0 MIT
remotipart 1.4.4 (git) critical 3.1/10 0 MIT
7 gems: 4 up to date, 0 outdated · 2 active, 2 stale, 2 archived · 0 vulnerabilities
Ruby 4.0.1 (latest)
Why still_active?
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.
bundle outdated |
bundler-audit |
libyear-bundler |
still_active |
|
|---|---|---|---|---|
| Outdated versions | Yes | - | Yes | Yes |
| Known vulnerabilities (CVEs) | - | Yes (ruby-advisory-db) | - | Yes (deps.dev + ruby-advisory-db) |
| Libyear drift | - | - | Yes | Yes |
| Last commit activity | - | - | - | Yes |
| Archived repo detection | - | - | - | Yes |
| OpenSSF Scorecard | - | - | - | Yes |
| Yanked version detection | - | - | - | Yes |
| Ruby version freshness | - | - | - | Yes (EOL + libyear) |
| GitLab support | - | - | - | Yes |
| CI quality gates | - | Exit code | - | Yes (4 flags) |
| Output formats | Text | Text | Text | Terminal, JSON, Markdown, SARIF, CycloneDX |
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.
Installation
gem install still_active
Quick Start
# audit your Gemfile (auto-detects output format)
still_active
# check specific gems
still_active --gems=rails,nokogiri,sidekiq
# CI pipeline: fail if any gem is critically stale or has vulnerabilities
still_active --fail-if-critical --fail-if-vulnerable
# ignore specific gems in CI checks
still_active --fail-if-warning --ignore=legacy_gem,internal_gem
# markdown table for pull requests or documentation
still_active --markdown
Usage
Authentication
still_active discovers a GitHub token in this order:
--github-oauth-token=TOKENCLI flagGITHUB_TOKENenvironment variable (CI convention)GH_TOKENenvironment variable (ghCLI convention)gh auth token(ifghis installed and authenticated)
Without a token, GitHub API calls are unauthenticated and rate-limited to 60 requests/hour — you will hit the limit on anything beyond a handful of gems. With a token the limit is 5000 requests/hour.
GitLab cascade mirrors GitHub: --gitlab-token → GITLAB_TOKEN → glab auth status --show-token. Optional for public repos, required for private ones.
CLI options
Usage: still_active [options]
all flags are optional
--gemfile=GEMFILE path to gemfile
--gems=GEM,GEM2,... Gem(s)
--terminal Coloured terminal output (default in TTY)
--markdown Markdown table output
--json JSON output (default when piped)
--sarif[=PATH] SARIF 2.1.0 output for GitHub Code Scanning
--cyclonedx[=PATH] CycloneDX SBOM output (stdout, or a file path)
--cyclonedx-version=VERSION CycloneDX spec version: 1.6 (default) or 1.7
--baseline=PATH Compare current state to baseline JSON; emit markdown deltas
--github-oauth-token=TOKEN GitHub OAuth token to make API calls
--gitlab-token=TOKEN GitLab personal access token for API calls
--simultaneous-requests=QTY Number of simultaneous requests made
--safe-range-end=YEARS maximum years since last activity considered safe (no warning)
--warning-range-end=YEARS maximum years since last activity that triggers a warning (beyond this is critical)
--fail-if-critical Exit 1 if any gem has critical activity warning
--fail-if-warning Exit 1 if any gem has warning or critical activity warning
--fail-if-vulnerable[=SEVERITY]
Exit 1 if any gem has vulnerabilities (optionally at or above SEVERITY)
--fail-if-outdated=LIBYEARS Exit 1 if any gem exceeds LIBYEARS behind latest
--ignore=GEM,GEM2,... Exclude gems from pass/fail checks (still shown in output)
--critical-warning-emoji=EMOJI
--futurist-emoji=EMOJI
--success-emoji=EMOJI
--unsure-emoji=EMOJI
--warning-emoji=EMOJI
-h, --help Show this message
-v, --version Show version
Output formats
Terminal (default on TTY) -- coloured table with summary line. Shown above.
JSON (default when piped) -- structured data for automation:
still_active --json --gemfile=spec/still_active/edge_case_gemfile/Gemfile
{
"gems": {
"async": {
"source_type": "rubygems",
"version_used": "2.36.0",
"latest_version": "2.36.0",
"repository_url": "https://github.com/socketry/async",
"last_commit_date": "2026-01-22 04:09:48 UTC",
"archived": false,
"scorecard_score": 7.1,
"vulnerability_count": 0,
"license": "MIT",
"libyear": 0.0
},
"nested_form": {
"source_type": "git",
"version_used": "0.3.2",
"repository_url": "https://github.com/ryanb/nested_form",
"last_commit_date": "2021-12-11 21:47:02 UTC",
"archived": true,
"scorecard_score": 3.3,
"vulnerability_count": 0
},
"local_gem": {
"source_type": "path",
"version_used": "0.1.0",
"scorecard_score": null,
"vulnerability_count": 0
}
},
"ruby": {
"version": "4.0.1",
"eol": false,
"latest_version": "4.0.1",
"libyear": 0.0
}
}
Markdown -- table for pull requests, documentation, or wikis:
still_active --markdown
| activity | up to date? | OpenSSF | vulns | name | version used | latest version | latest pre-release | last commit | libyear | license |
|---|---|---|---|---|---|---|---|---|---|---|
| ✅ | 7.1/10 | ✅ | async | 2.36.0 (2026/01) | 2.36.0 (2026/01) | ❓ | 2026/01 | 0.0y | MIT | |
| 🚩 | ✅ | 3.6/10 | ✅ | backbone-rails | 1.2.3 (2016/02) | 1.2.3 (2016/02) | ❓ | 2016/02 | 0.0y | MIT |
| ❓ | ❓ | ❓ | ✅ | local_gem | 0.1.0 (path) | ❓ | ❓ | ❓ | - | - |
| 🚩 | ❓ | 3.3/10 | ✅ | nested_form | 0.3.2 (git) | ❓ | ❓ | 2021/12 | - | MIT |
Ruby 4.0.1 (latest) ✅
CycloneDX -- a standards-track SBOM so your dependency graph and still_active's signals flow into Trivy, Dependency-Track, or Snyk:
still_active --cyclonedx # CycloneDX 1.6 to stdout
still_active --cyclonedx=sbom.json # write to a file
still_active --cyclonedx --cyclonedx-version=1.7
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.
SARIF output (GitHub Code Scanning)
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.
See it live: this repo audits itself on every push. Browse the live findings in the Code Scanning Security tab — currently 2×
SA005(low OpenSSF Scorecard).
still_active --sarif # writes still_active.sarif.json
still_active --sarif=path/to/out.sarif.json
still_active --sarif=- # stdout
Easy mode — use the still_active-action wrapper:
permissions:
contents: read
security-events: write # required for SARIF upload
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with: { ruby-version: '3.4' }
- uses: SeanLF/still_active-action@v0
with:
github-token: ${{ github.token }}
sarif: still_active.sarif.json
- uses: github/codeql-action/upload-sarif@v3
if: always()
with: { sarif_file: still_active.sarif.json }
Plain bundle exec if you'd rather pin still_active in your Gemfile:
- run: bundle exec still_active --sarif
env:
GITHUB_TOKEN: ${{ github.token }}
- uses: github/codeql-action/upload-sarif@v3
if: always()
with: { sarif_file: still_active.sarif.json }
Rule reference (SA001–SA007) and how to suppress: see docs/rules.md.
Baseline diff (PR review)
--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?
# Locally — capture from main, compare to your branch
git checkout main && still_active --json > /tmp/main.json
git checkout my-branch && still_active --baseline=/tmp/main.json
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.).
The diff supersedes --sarif, --terminal, --markdown, and --json when set.
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.
Alongside dependency-review-action
GitHub's first-party 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:
dependency-review-action |
still_active |
|
|---|---|---|
| Platform | GitHub.com / GHES only | Any CI |
| Languages | Multi (GitHub dep graph) | Ruby |
| Vulnerabilities | GHSA | deps.dev + ruby-advisory-db (merged) |
| Licenses | Yes (allow/deny gating) | Surfaced (no gating) |
| OpenSSF Scorecard | Yes (display) | Yes (display + threshold) |
| Last-commit activity | - | Yes |
| Archived repo detection | - | Yes |
| Libyear drift | - | Yes |
| Ruby EOL detection | - | Yes |
| Yanked version detection | - | Yes |
| Diff vs base | Native (GitHub API) | --baseline=FILE |
| Output | Inline PR annotations | Terminal / Markdown / JSON / SARIF / CycloneDX |
Run both: let dependency-review-action gate CVEs and licenses, and still_active add the maintenance lens on the same PR.
on: pull_request
jobs:
dependency-review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/dependency-review-action@v4
with:
fail-on-severity: high
show-openssf-scorecard: true
maintenance-review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with: { ruby-version: ".ruby-version", bundler-cache: true }
- uses: SeanLF/still_active-action@v0
with:
fail-if-critical: true
CI quality gating
Use exit-code flags to fail CI pipelines based on dependency status:
# fail on critically stale or archived gems
still_active --fail-if-critical --json
# fail on any stale, critical, or archived gem
still_active --fail-if-warning --json
# fail if any gem has known vulnerabilities
still_active --fail-if-vulnerable --json
# fail only on high/critical severity vulnerabilities
still_active --fail-if-vulnerable=high --json
# fail if any gem is more than 3 libyears behind
still_active --fail-if-outdated=3 --json
# combine flags and exclude known exceptions
still_active --fail-if-warning --fail-if-vulnerable --ignore=legacy_gem --json
Activity thresholds
Activity is determined by the most recent signal across last commit date, latest release date, and latest pre-release date:
- ok: last activity within 1 year (configurable with
--safe-range-end) - stale: last activity between 1 and 3 years ago (configurable with
--warning-range-end) - critical: last activity over 3 years ago
Data sources
- Versions, release dates, and licenses from RubyGems.org or GitHub Packages
- Last commit date and archived status from the GitHub or GitLab API
- OpenSSF Scorecard, vulnerability counts, and CVSS severity from Google's deps.dev API
- Additional advisories from ruby-advisory-db, merged in when
bundler-auditis installed alongside (runbundle audit updateto keep its checkout current) - Ruby version freshness from endoflife.date
Configuration defaults
| Option | Default | Description |
|---|---|---|
output_format |
auto-detect | Coloured terminal on TTY, JSON when piped |
safe_range_end |
1 year | Last activity within this range is "ok" |
warning_range_end |
3 years | Last activity within this range is "stale"; beyond is "critical" |
simultaneous_requests |
10 | Concurrent API requests |
Development
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.
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.
To install this gem onto your local machine, run bundle exec rake install. New versions are published automatically to rubygems.org when a GitHub Release is created (via trusted publishing).
Contributing
Bug reports and pull requests are welcome.
License
The gem is available as open source under the terms of the MIT License.