#stale #audit #cargo #dependencies

bin+lib dep-age

Check how old your dependencies are — for Cargo.toml, package.json, pyproject.toml, requirements.txt, go.mod, docker-compose.yml, Gemfile, and composer.json

5 releases

0.1.4 Apr 24, 2026
0.1.3 Apr 16, 2026
0.1.2 Apr 15, 2026
0.1.1 Apr 13, 2026
0.1.0 Apr 13, 2026

#10 in #stale

MIT license

170KB
4K SLoC

dep-age

Crates.io Crates.io Downloads npm CI License

Check how old your dependencies are — for Cargo.toml, package.json, pyproject.toml, requirements.txt, go.mod, docker-compose.yml, Gemfile, and composer.json.

See at a glance which packages haven't been updated in months or years. Spot stale and abandoned dependencies before they become a security or compatibility problem.

  ✓  tokio                              "1"            3mo       aging    crates
  ✓  serde                              "1"            1mo       fresh    crates
  !  toml                               "0.5"          1.8y      stale    crates
  ✗  time                               "0.1"          3.2y      ancient  crates

Install

Cargo

cargo install dep-age

Homebrew

brew tap Ayyankhan101/dep-age
brew install dep-age

npm

npm install -g dep-age

CLI usage

# Auto-detect Cargo.toml or package.json in current directory
dep-age

# Check a specific file
dep-age Cargo.toml
dep-age path/to/package.json
dep-age path/to/pyproject.toml
dep-age path/to/requirements.txt
dep-age path/to/go.mod
dep-age path/to/docker-compose.yml

# Skip dev-dependencies
dep-age --no-dev

# Only show specific statuses
dep-age --filter ancient
dep-age --filter stale
dep-age --filter aging

# Output raw JSON (great for CI)
dep-age --json

# Fail only on stale+ancient (default: ancient)
dep-age --fail-on stale

# Use cached registry responses (1h TTL)
dep-age --cache

# Custom age thresholds (days)
dep-age --fresh 60 --aging 180 --stale 540

# Ignore specific packages
dep-age --ignore time --ignore old-crate

# Sort results
dep-age --sort age        # by age (default)
dep-age --sort name       # alphabetically
dep-age --sort status     # grouped by status

# Output formats
dep-age --format csv      # CSV output (pipeable to spreadsheets)
dep-age --format json     # JSON output (alternative to --json)
dep-age --format ndjson   # Newline-delimited JSON (streaming-friendly)
dep-age --format github-checks  # GitHub Actions annotations
dep-age --format junit          # JUnit XML for CI systems
dep-age --format sarif         # SARIF for GitHub Advanced Security

# Config file (create .dep-age.toml in project root)
# [tool.dep-age]
# fresh = 60
# aging = 180
# stale = 540
# no-dev = true
# fail-on = "stale"
# ignore = ["time", "old-crate"]

# Custom config file
dep-age --config path/to/dep-age.toml

# Check mode - quiet, exit code only
dep-age --check

# Diff - show changes since last run
dep-age --diff

# Ignore file: create a `.dep-age-ignore` in your project root
# One package name per line, # comments supported

# Control parallelism (default: 10)
dep-age --concurrency 20

Status thresholds

Icon Status Age
fresh < 90 days
~ aging 90 days – 1 yr
! stale 1 yr – 2 yrs
ancient 2+ years

The CLI exits with code 1 if any ancient packages are found — useful for CI pipelines.


Library usage

Add to your Cargo.toml:

[dependencies]
dep-age = "0.1.4"
tokio = { version = "1", features = ["full"] }

Check a Cargo.toml

use dep_age::{check_cargo_toml, CheckOptions};

#[tokio::main]
async fn main() {
    let opts = CheckOptions::default();
    let summary = check_cargo_toml("Cargo.toml", &opts).await.unwrap();

    println!("Total packages: {}", summary.total);
    println!("Ancient:        {}", summary.ancient);
    println!("Stale:          {}", summary.stale);

    if let Some(oldest) = &summary.oldest {
        println!("Oldest: {} ({} days)", oldest.name, oldest.days_since_publish.unwrap_or(0));
    }

    for result in &summary.results {
        println!("{}{} ({})", result.name, result.status.as_str(),
            result.days_since_publish.map(|d| format!("{}d", d)).unwrap_or_default()
        );
    }
}

Check a package.json

use dep_age::{check_package_json, CheckOptions};

#[tokio::main]
async fn main() {
    let opts = CheckOptions {
        include_dev: false,   // skip devDependencies
        concurrency: 20,      // more parallel requests
        ..CheckOptions::default()
    };

    let summary = check_package_json("package.json", &opts).await.unwrap();
    println!("Stale npm packages: {}", summary.stale);
}

Check a single package

use dep_age::{check_crate, check_npm_package, check_pypi_package, CheckOptions};

#[tokio::main]
async fn main() {
    let opts = CheckOptions::default();

    let result = check_crate("serde", "1", &opts).await;
    println!("{}: {} days old", result.name, result.days_since_publish.unwrap_or(0));

    let result = check_npm_package("lodash", "^4.17.21", &opts).await;
    println!("{}: {}", result.name, result.status.as_str());

    let result = check_pypi_package("requests", ">=2.28", &opts).await;
    println!("{}: {}", result.name, result.status.as_str());
}

Check a pyproject.toml or requirements.txt

use dep_age::{check_pyproject_toml, check_requirements_txt, CheckOptions};

#[tokio::main]
async fn main() {
    let opts = CheckOptions::default();

    let summary = check_pyproject_toml("pyproject.toml", &opts).await.unwrap();
    println!("Total Python deps: {}", summary.total);

    let summary = check_requirements_txt("requirements.txt", &opts).await.unwrap();
    println!("Ancient Python packages: {}", summary.ancient);
}

Check a go.mod

use dep_age::{check_go_mod, CheckOptions};

#[tokio::main]
async fn main() {
    let opts = CheckOptions::default();

    let summary = check_go_mod("go.mod", &opts).await.unwrap();
    println!("Total Go modules: {}", summary.total);
    println!("Stale: {}", summary.stale);
}

Check a Docker Compose file

use dep_age::{check_docker_compose, CheckOptions};

#[tokio::main]
async fn main() {
    let opts = CheckOptions::default();

    let summary = check_docker_compose("docker-compose.yml", &opts).await.unwrap();
    println!("Total Docker images: {}", summary.total);
    println!("Ancient images: {}", summary.ancient);
}

CheckOptions

Field Type Default Description
include_dev bool true Include dev-dependencies
concurrency usize 10 Parallel registry requests
threshold_fresh i64 90 Days below which = fresh
threshold_aging i64 365 Days below which = aging
threshold_stale i64 730 Days below which = stale
ignore_list Vec<String> [] Package names to skip
crates_base_url Option<String> None Override crates.io API URL
npm_base_url Option<String> None Override npm registry URL
pypi_base_url Option<String> None Override PyPI API URL
registry_cache Option<RegistryCache> None Use cached registry responses
on_progress Arc<dyn Fn(DepResult)> None Callback per-package results

DepResult

pub struct DepResult {
    pub name: String,
    pub version_spec: String,
    pub latest_version: String,
    pub published_at: Option<DateTime<Utc>>,
    pub days_since_publish: Option<i64>,
    pub status: Status,     // Fresh | Aging | Stale | Ancient | Error(String)
    pub registry: Registry, // Crates | Npm | Pypi
}

DepAgeSummary

pub struct DepAgeSummary {
    pub results: Vec<DepResult>,
    pub total: usize,
    pub fresh: usize,
    pub aging: usize,
    pub stale: usize,
    pub ancient: usize,
    pub errors: usize,
    pub oldest: Option<DepResult>,
    pub checked_at: DateTime<Utc>,
}

Convenience methods

// Check if all packages are fresh (no aging, stale, ancient, or errors)
if summary.is_all_fresh() {
    println!("All dependencies are up to date!");
}

CI usage

GitHub Actions

- name: Check dependency ages
  run: dep-age --no-dev --fail-on stale

GitHub Actions with SARIF (Advanced Security)

- name: Check dependency ages
  run: dep-age --format sarif --output dep-age.sarif

- name: Upload SARIF
  uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: dep-age.sarif

GitHub Actions with GitHub Checks

- name: Check dependency ages
  run: dep-age --format github-checks >> $GITHUB_OUTPUT

JSON output + jq

# Fail if any packages are ancient
dep-age --json | jq 'if .ancient > 0 then error("ancient packages found") else . end'

# Or just let dep-age exit 1 automatically
dep-age --no-dev && echo "all good"

# Save a report
dep-age --json > dep-age-report.json

JUnit XML (Jenkins, etc.)

dep-age --format junit > test-results.xml

With cache (faster re-runs)

# Cache registry responses (1h TTL) for faster re-runs
dep-age --cache --no-dev

Diff tracking

# First run - establishes baseline
dep-age --no-dev

# Second run - shows changes
dep-age --no-dev --diff

Contributing

  1. Fork and clone the repo
  2. Run cargo fmt && cargo clippy -- -D warnings && cargo test
  3. Open a PR with a clear description of the change

All PRs require green CI (formatting, clippy, tests) before merge.


Security

To report a security vulnerability, please email the maintainer. Do not open a public issue.


License

MIT

Dependencies

~13–29MB
~356K SLoC