npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

deployhealth-scan

v0.5.0

Published

Find env var drift in a repo (read in code but never defined, defined but never read, out of sync between env files) and report it to deployhealth.

Readme

deployhealth-scan

Find env var drift in a repository and report it to deployhealth:

  • MISSING: read in code (process.env.X, os.environ["X"], os.Getenv("X"), ENV["X"], env('X'), System.getenv("X"), @Value("${X}")) but defined in no env file;
  • UNUSED: defined in an env file but never read;
  • MISMATCH: in .env but not .env.example, or the reverse.

It scans JavaScript/TypeScript (.js, .jsx, .mjs, .cjs, .ts, .tsx, .mts, .cts), Python (including pydantic-settings classes), Go, Ruby, PHP (with Laravel and Symfony) and Java/Kotlin (.java, .kt, .kts, with Spring), respects .gitignore, and treats each folder with its own env files as a separate scope (monorepos). One file, no dependencies, Node 20+.

Reads JS/TS, Python, Go, Ruby, PHP and Java/Kotlin. Rust, C# and others aren't read yet: on a repo in those, the pull request check and the CLI say they can't check it instead of passing it.

On a directory with no such source file (after the rules below), it prints No JS/TS, Python, Go, Ruby, PHP or Java/Kotlin source files found: deployhealth can't check this directory yet., reports no UNUSED variables (nothing it reads could use them), still reports MISMATCH, and exits 0. With --json the line goes to stderr, and the JSON says "can_check": false with "source_files": 0.

Env files it reads: .env, .env.local, .env.development, .env.production, .env.test and their .local variants, plus declaration files: .env.example, .env.sample, .env.template, .env.dist, .env.defaults, example.env, sample.env, env.example and .env.<name>.example / .sample / .template (e.g. .env.appStore.example). A commented-out # KEY= line in a declaration file declares KEY too.

A first scan stays quiet:

  • Names the platform or runtime provides are skipped: NODE_ENV, CI, npm_*, GitHub Actions' default variables (GITHUB_SHA, GITHUB_REF_NAME, …, not your own GITHUB_ names), RUNNER_*, VERCEL_*, RAILWAY_*, RENDER_*, FLY_* and a few more. --no-default-ignore checks them too; --dry-run lists what was skipped.
  • A read with a default on the same line isn't MISSING: process.env.X ?? "a" or || "a", os.getenv("X", "a"), os.environ.get("X", "a"), os.getenv("X") or "a", ENV.fetch("X", "a"), ENV.fetch("X") { … }, ENV["X"] || "a", env('X', 'a'), env('X') ?? 'a' / ?: 'a', getenv('X') ?: 'a', $_ENV['X'] ?? 'a', System.getenv().getOrDefault("X", "a"), Kotlin's System.getenv("X") ?: "a", Optional.ofNullable(System.getenv("X")).orElse("a"), @Value("${X:a}"). Such variables are counted as optional (one OPTIONAL line; --show-optional lists them). (A default of undefined, null, None or nil is no default, nor is PHP's ?: throw … or Kotlin's ?: error(…).)
  • Tests, fixtures and test tooling are skipped: test/, tests/, __tests__/, spec/, e2e/, fixtures/, __fixtures__/, testdata/, playwright/, cypress/, mocks/, __mocks__/ and testing/ directories (their env files too, so a fixture's .env never becomes a scope), and *.test.*, *.spec.*, *.e2e.*, *.e2e-spec.*, *.cy.*, *_test.go, test_*.py, *_test.py, conftest.py, *_spec.rb, *Test.php, phpunit.xml, *Test(s).java|kt, *IT.java|kt, Spring's application-test.*, Gradle's *.gradle.kts build scripts, and playwright/vitest/jest/cypress config files, vitest.workspace.* and vitest/jest setup files. They're only read to see which variables they use, so a variable only tests read isn't UNUSED. --include-tests scans them like any other file.
  • Vendored and generated code is never read: node_modules, dist, .next, venv, .venv, .yarn, vendor, third_party, bower_components, out, coverage, .turbo, .vercel, .output, .svelte-kit, .nuxt, .cache, .pnpm-store, __pycache__, site-packages, .phpunit.cache, .gradle and .mvn directories, bootstrap/cache/ and public/build/, Laravel's storage/ next to artisan (never opened: cached config can hold values), Maven's target/ next to pom.xml, Gradle's build/ and target/ next to a Gradle build file, .pnp.cjs, .pnp.loader.mjs, *.min.js (.mjs, .cjs), and any source file over 512 KB. --dry-run says what it skipped.
  • Docker Compose interpolation counts as a use: ${VAR}, ${VAR:-x}, ${VAR:?x}, $VAR, … in docker-compose*.yml / compose*.yaml keep an env entry Compose consumes from being UNUSED. They never make MISSING rows (Compose often gets values from the shell or CI).
  • Laravel and Symfony: next to artisan, the env vars Laravel's framework and skeleton read (BCRYPT_ROUNDS, BROADCAST_CONNECTION, …) are never UNUSED, and a read of one is never MISSING (except APP_KEY). $_SERVER['X'] marks X used, never MISSING. Symfony's %env(X)% in config/**/*.yaml is a read (%env(default:param:X)% is optional).
  • Spring: ${X} in application* / bootstrap* .properties and .yml files is a read (${X:default} is optional); dotted names like ${server.port} are properties, not env vars. Where the code uses Spring, a declared name Spring binds to a property isn't UNUSED: the env form of a key in that config (mail.host → MAIL_HOST), anything starting with SPRING_, SERVER_, MANAGEMENT_ or LOGGING_, and a @ConfigurationProperties("app.mail") prefix (APP_MAIL_…). That only ever marks names used, never MISSING.
  • One row per variable per line, even when a line reads it twice, and a quoted 'process.env.X' (a bundler define key) isn't a read. Same-line destructuring counts: const { X, Y: y, Z = "a" } = process.env reads all three, Z as optional.
  • A folder with no env file at all gets one line, "No .env.example …: N variables referenced", instead of a MISSING row per reference; deployhealth's handoff offers them as a starting .env.example.

pydantic-settings

In a .py file, a class whose bases include BaseSettings (or a subclass of one in the same file) reads one env var per annotated field: database_url: str is DATABASE_URL, after a literal env_prefix (model_config = SettingsConfigDict(env_prefix="APP_") or class Config). A string alias= / validation_alias= replaces the name; each AliasChoices(...) string counts and is optional. A field with a default is optional; = None counts as a default only when the type allows None (str | None, Optional[str]). ClassVar, _private fields, model_config and fields typed as a model from the same file are skipped.

Known misses

Same-line, regex-based scanning misses some things on purpose:

  • reads split across lines (including a destructuring pattern spread over several lines), and dynamic keys (process.env[name], os.environ[key]);
  • pydantic-settings bases defined in another file, prefixes or aliases held in variables, AliasPath, and env_nested_delimiter's nested names;
  • Compose's env_file: (it passes a whole file into a container) and environment: keys;
  • env reads in other file types: Rails' ERB in config/*.yml, .rake files, shell scripts, Dockerfiles and CI workflows;
  • turbo.json's env lists, which aren't declarations.

Try it locally

npx [email protected] --dry-run          # grouped findings with file:line
npx [email protected] --dry-run --json   # the same, as JSON

With --dry-run nothing leaves your machine.

Report every push from GitHub Actions

Create a project in deployhealth, add its ingest token as a repository secret named DEPLOYHEALTH_TOKEN (Settings → Secrets and variables → Actions → Secrets tab → New repository secret; not a Variable, not an environment secret), then add .github/workflows/deployhealth.yml:

name: deployhealth

# Must not run on pull_request events from forks: the job reads a repository secret.
on:
  push:
    branches: [main]

jobs:
  env-scan:
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@v5
        with:
          persist-credentials: false
      - uses: actions/setup-node@v5
        with:
          node-version: 22
          package-manager-cache: false
      - name: Scan env vars and report to deployhealth
        env:
          DEPLOYHEALTH_TOKEN: ${{ secrets.DEPLOYHEALTH_TOKEN }}
        run: |
          npx --yes [email protected] \
            --url https://deployhealth.dev \
            --token "$DEPLOYHEALTH_TOKEN" \
            --sha "$GITHUB_SHA" \
            --branch "$GITHUB_REF_NAME"

Pin the version (@0.5.0) so an update never runs in your CI unreviewed. For a self-hosted deployhealth, change --url to your instance.

What it sends

The commit sha and branch, and for each finding the variable name, file:line and env file name, plus every referenced variable name with the env files that define it (and whether the code has a default), and each scope's env file names. Never values: the scanner reads env files for the names they define; values are neither sent nor printed.

Options

--url <url>          deployhealth base URL (required unless --dry-run)
--token <token>      the project's ingest token, dh_... (required unless --dry-run;
                     default: the DEPLOYHEALTH_TOKEN environment variable)
--sha <sha>          commit being deployed (default: git rev-parse HEAD)
--branch <name>      branch being deployed (default: current git branch)
--dir <path>         directory to scan (default: current directory)
--ignore <glob>      skip variables matching a glob, e.g. NEXT_PUBLIC_* (repeatable)
--no-default-ignore  also check names the platform or runtime provides (NODE_ENV,
                     CI, GITHUB_SHA, npm_*, VERCEL_*, RAILWAY_*, RENDER_*, FLY_*, ...)
--exclude <pattern>  skip paths matching a gitignore-style pattern (repeatable)
--include-tests      also scan tests, fixtures and test tooling (test/, tests/, e2e/,
                     fixtures/, playwright/, testing/, *.test.*, *.spec.*, *.e2e.*,
                     *_test.go, vitest.config.*, ...)
--dry-run            print the findings instead of sending them
--show-optional      with --dry-run, list each variable read with a default (OPTIONAL)
--json               with --dry-run, print JSON (always lists them)
-v, --version        print the version
-h, --help           show this help

The token can come from the DEPLOYHEALTH_TOKEN environment variable instead of --token, which keeps it out of process listings. A --url that is plain http: to anything but this machine prints a warning: the token and the report would travel unencrypted.

Exit codes: 0 success, 1 the report failed (network or HTTP error), 2 usage error.

License

MIT. The deployhealth web app and worker are licensed separately (FSL-1.1-MIT); see the repository.

The name "deployhealth" and its logo are not licensed under MIT or FSL-1.1-MIT; please don't use them for your own product or service.