node-version-reader
v1.0.2
Published
A Node.js CLI tool that reads and checks version information from package.json files, for enforcing version bumps in pull requests and showing which packages had their version changed.
Maintainers
Readme
NodeVersionReader
A Node.js CLI tool for reading version information from package.json files,
for enforcing version bumps in pull requests, and for showing which packages had their version changed.
Installation
npm install --global node-version-readerOr from a local build:
npm run build
npm install --global .Commands
node-version [command] [options]
Commands:
read Reads and displays version information from package.json files. (default)
check Checks that every package whose source files have changed has had its version bumped.
diff Shows packages whose version has changed (or that are new) relative to a base branch.node-version read — read versions (default)
Reads and displays version information. This is the default command: running
node-version with no subcommand is equivalent to node-version read.
# Both forms are equivalent:
node-version [--input <path>] [options]
node-version read [--input <path>] [options]Options
| Option | Short | Description |
| -------- | ------- | ------------- |
| --input | -i | Path to a package.json file or a folder. Defaults to the current directory. |
| --output | -o | Output format: json (default), table, list, or version (single package only). |
| --filter | -f | Filter in the form key=Value. Value can be a regex. Repeatable. |
| --schema | | Print the JSON schema for --output json and exit. Defaults to false. |
Locating package.json files
--input accepts:
- A path to a single
package.jsonfile. - A path to a workspace root
package.json(one that has a"workspaces"field) — all member packages resolved from the workspace globs are read instead of the root itself. - A path to a folder — every
package.jsonfound recursively underneath is read.node_modules,.git,dist,build,out, andcoveragedirectories are always skipped. - Nothing — the current directory is used.
Version resolution
Unlike .csproj files, package.json has a single "version" field (there is no
prefix/suffix split):
- If
"version"is set, it is used as-is. - Otherwise the version defaults to
0.0.0.
Examples
# Current directory – JSON output (default, both forms are equivalent)
node-version
node-version read
# Specific package.json file – table output
node-version read --input package.json --output table
node-version read -i package.json -o table
# A monorepo root – every workspace member is read
node-version read --input package.json
# Only packages that are publishable (not marked private)
node-version read --filter "private=^(?!true$)"
# Combine multiple filters (all must match)
node-version read -i . -f "engines.node=>=18" -f "private=^(?!true$)"Sample JSON output
[
{
"name": "my-library",
"version": "2.1.0-rc.1",
"major": 2,
"minor": 1,
"patch": 0,
"suffix": "rc.1"
},
{
"name": "my-app",
"version": "1.0.0",
"major": 1,
"minor": 0,
"patch": 0,
"suffix": null
}
]Sample table output
| Name | Version | Major | Minor | Patch | Suffix |
| ----------- | ---------- | ----- | ----- | ----- | ------ |
| my-library | 2.1.0-rc.1 | 2 | 1 | 0 | rc.1 |
| my-app | 1.0.0 | 1 | 0 | 0 | |Sample list output
my-library 2.1.0-rc.1
my-app 1.0.0Sample version output
2.1.0-rc.1node-version check — enforce version bumps in PRs
Checks that every package whose source files have changed (compared to a base branch) has had its version bumped. Designed to run as a CI gate on pull requests.
node-version check [--base <ref>] [--input <path>] [--head <ref>] [--output <format>] [--filter <key=Value>]...
# Short aliases (--base defaults to origin/main):
node-version check [-b <ref>] [-i <path>] [--head <ref>] [-o <format>] [-f <key=Value>]...Options
| Option | Short | Required | Description |
| -------- | ------- | ---------- | ------------- |
| --input | -i | | Path to a package.json file or a folder. Defaults to the current directory. |
| --base | -b | | The git ref to compare against. Defaults to origin/main. |
| --head | | | The git ref for the current state. Defaults to HEAD. |
| --output | -o | | Output format: json (default), table, or version (single package only). |
| --filter | -f | | Filter in the form key=Value. Only matching packages are checked. Value can be a regex. Repeatable. |
Exit codes
| Code | Meaning |
| ------ | --------- |
| 0 | All affected packages have been version-bumped (or no relevant files changed). |
| 1 | At least one affected package has not been bumped — the check failed. |
| 2 | Usage or argument error (bad input path, git not found, etc.). |
How it works
- Locates all
package.jsonfiles from<input>. - Builds a dependency graph: for each package, which files it owns and which other
local packages it references via
dependencies,devDependencies,peerDependencies, oroptionalDependencies(matched by name against other located packages). - Collects changed files by unioning: committed diff (
<base>...<head>), staged changes, unstaged tracked changes, and untracked new files — so it works both in a PR context and with local uncommitted modifications. - Determines affected packages transitively: if a library changes, every package that depends on it (directly or indirectly) is also considered affected.
- For each affected package, reads the version on
<base>(viagit show) and compares it to the version in the working tree. - Reports the result and exits with code
1if any version was not bumped.
Examples
# Check current directory against origin/main (default, both are equivalent)
node-version check
node-version check --base origin/main
# Scope to a specific package.json (workspace root or single package)
node-version check --input package.json --base origin/main
node-version check -i package.json -b origin/main
# Table output
node-version check --input package.json --base origin/main --output table
# Single package, bare version output (useful for scripts)
node-version check --input packages/my-lib/package.json --base origin/main --output version
# Only check packages that are publishable (not marked private)
node-version check --input package.json --base origin/main --filter "private=^(?!true$)"Sample JSON output
[
{
"name": "my-lib",
"filePath": "packages/my-lib/package.json",
"headVersion": "2.0.0",
"baseVersion": "1.0.0",
"status": "Ok"
},
{
"name": "my-app",
"filePath": "packages/my-app/package.json",
"headVersion": "3.1.0",
"baseVersion": "3.1.0",
"status": "BumpRequired"
}
]Possible status values:
| Value | Meaning |
| ------- | --------- |
| Ok | No relevant files changed, or the version was bumped. |
| BumpRequired | Files changed but the version is the same as on the base branch. |
| NewProject | The package did not exist on the base branch — no bump required. |
Sample table output
| Name | HeadVersion | BaseVersion | Status |
|-------|-------------|-------------|--------------|
| my-lib| 2.0.0 | 1.0.0 | Ok |
| my-app| 3.1.0 | 3.1.0 | BumpRequired |GitHub Actions integration
name: Check version bumps
on:
pull_request:
branches: [main]
jobs:
check-versions:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # full history is required for git diff
- uses: actions/setup-node@v4
with:
node-version: 20.x
- name: Install node-version-reader
run: npm install --global node-version-reader
- name: Check version bumps
run: node-version check --input package.json --base origin/mainImportant:
fetch-depth: 0(or at least enough history to reach the base branch) is required; a shallow clone will causegit diffto fail.
node-version diff — show version changes relative to a base branch
Shows all packages whose version has changed (or that are brand-new) compared to a base branch.
Unlike check, this command never exits with a non-zero code based on results — it is a
pure informational diff, useful for release notes, changelogs, or scripting.
node-version diff [--base <ref>] [--input <path>] [--head <ref>] [--output <format>] [--filter <key=Value>]...
# Short aliases (--base defaults to origin/main):
node-version diff [-b <ref>] [-i <path>] [--head <ref>] [-o <format>] [-f <key=Value>]...Options
| Option | Short | Description |
| -------- | ------- | ------------- |
| --input | -i | Path to a package.json file or a folder. Defaults to the current directory. |
| --base | -b | The git ref to compare against. Defaults to origin/main. |
| --head | | The git ref for the current state. Defaults to HEAD. |
| --output | -o | Output format: json (default), table, list, or version (single package only). |
| --filter | -f | Filter in the form key=Value. Only matching packages are considered. Value can be a regex. Repeatable. |
Exit codes
| Code | Meaning |
| ------ | --------- |
| 0 | Command completed successfully (regardless of how many packages changed). |
| 2 | Usage or argument error (bad input path, git not found, etc.). |
How it works
Uses the same git/dependency-graph pipeline as check (steps 1–4 are identical), but at step 5
only keeps packages whose version on <head> differs from the version on <base> (or
packages that are brand-new). Packages whose version is unchanged are silently omitted.
Examples
# Show changed versions against origin/main (default)
node-version diff
node-version diff --base origin/main
# Scope to a specific package.json
node-version diff --input package.json --base origin/main
node-version diff -i package.json -b origin/main
# Table output
node-version diff --input package.json --base origin/main --output table
# Simple list output – handy for release notes
node-version diff --input package.json --base origin/main --output list
# Only packages that are publishable (not marked private)
node-version diff --input package.json --base origin/main --filter "private=^(?!true$)"Sample JSON output
[
{
"name": "my-lib",
"filePath": "packages/my-lib/package.json",
"headVersion": "2.0.0",
"baseVersion": "1.0.0",
"status": "Bumped"
},
{
"name": "my-new-lib",
"filePath": "packages/my-new-lib/package.json",
"headVersion": "1.0.0",
"baseVersion": null,
"status": "NewProject"
}
]Possible status values:
| Value | Meaning |
| ------- | --------- |
| Bumped | The version was bumped relative to the base branch. |
| NewProject | The package did not exist on the base branch. |
Sample table output
| Name | HeadVersion | BaseVersion | Status |
|-------------|-------------|-------------|------------|
| my-lib | 2.0.0 | 1.0.0 | Bumped |
| my-new-lib | 1.0.0 | | NewProject |Sample list output
my-lib 2.0.0
my-new-lib 1.0.0Filtering
Filters take the form key=Value, where key is a JSON property name (matched
case-insensitively, at any depth in the package.json document — including nested objects such
as engines.node or publishConfig.access, and array elements) and Value is a regular
expression that must match the stringified value.
If key does not exist anywhere in the document, the pattern is tested against an empty
string rather than automatically failing the filter. This matters for fields like private
that have a well-defined falsy default when absent — an absent "private" field is treated the
same as "private": false for filtering purposes.
# Only packages marked as private
node-version read --filter "private=^true$"
# Only packages targeting Node.js >= 18 (nested under "engines")
node-version read --filter "node=>=18"
# Combine filters – ALL must match
node-version read -f "private=^(?!true$)" -f "version=^[2-9]"⚠️ Anchor negative-lookahead filters with ^...$
A common mistake is writing an unanchored negation like private=(?!true), expecting it to
match everything that isn't "true". It doesn't: .test() succeeds if a match exists anywhere
in the string, and a zero-width lookahead can always succeed starting at a later position — so
/(?!true)/i.test("true") is actually true, meaning this filter never excludes anything.
Always anchor negative lookaheads with ^ and $:
# ❌ WRONG – never excludes anything, (?!true) matches even inside "true"
node-version read --filter "private=(?!true)"
# ✅ CORRECT – anchored, actually excludes private:true packages
node-version read --filter "private=^(?!true$)"Development
# Install dependencies
npm install
# Type-check
npm run typecheck
# Lint
npm run lint
# Run tests
npm test
# Build
npm run build
# Run the CLI without building (via tsx)
npm run dev -- read --input package.jsonProject layout
src/
models/ # Plain TypeScript types: PackageVersionInfo, CheckResult, DiffResult, OutputFormat
services/ # PackageJsonLocator, PackageJsonParser, FilterParser, DependencyGraphService,
# GitService, DiffService, Formatter, JsonSchemaProvider
cli.ts # Commander-based CLI wiring (read/check/diff commands)
index.ts # Public library entry point (re-exports models + services)
tests/
models/ # Unit tests for model helpers
services/ # Unit tests for each service
integration/ # End-to-end pipeline tests
fixtures/ # In-memory package.json fixtures
helpers/ # Temp-file/dir test helpersCI / CD
The repository uses two GitHub Actions workflows.
check-version-bump.yml — PR gate
Runs on every pull request targeting main. Builds the tool from source and
runs node-version check to ensure every publishable package that changed has
had its version bumped.
node-version check --input package.json --filter "private=^(?!true$)"The PR must pass this check before merging.
build-test-pack-publish.yml — publish on push to main
Runs automatically on every push to main and can also be triggered manually.
| Step | Details |
| ------ | --------- |
| Install | npm ci |
| Typecheck | npm run typecheck |
| Lint | npm run lint |
| Test | npm test |
| Build | npm run build |
| Check version bump | Runs node-version check on pull requests against the PR base branch — blocks the build if the package version was not bumped |
| Tag & Publish | Tags the commit v<version> and publishes to npm using trusted publishing (OIDC) |
| GitHub Release | Creates a GitHub release on the new tag with auto-generated notes |
Publishing authentication — npm trusted publishing (OIDC)
The publish job authenticates to npm via trusted publishing instead of a long-lived token. This requires:
- A trusted publisher configured on npmjs.com for the
node-version-readerpackage:npmjs.com→ package → Settings → Trusted Publisher → GitHub Actions, with:- Organization or user:
wertzui - Repository:
NodeVersionReader - Workflow filename:
build-test-pack-publish.yml(filename only, must match exactly) - Allowed actions:
npm publish
- Organization or user:
id-token: writepermission on thepublishjob (already set in the workflow) so GitHub Actions can issue an OIDC token.- npm CLI ≥ 11.5.1 and Node ≥ 22.14.0 — the workflow uses Node 24.x, which already bundles a
sufficiently new npm CLI. Older npm versions don't know how to request/exchange OIDC tokens
and fail with
ENEEDAUTH.
No NPM_TOKEN secret is required (or used) for publishing. Provenance attestations are generated
automatically since trusted publishing is used from a public repository/package.
Do not run
npm install -g npm@latestto "ensure" a new-enough npm CLI. Self-upgrading npm in place is known to corrupt its ownnode_modulestree (missing optional dependencies such assigstore, whichlibnpmpublishneeds for provenance), causingnpm publishto fail withMODULE_NOT_FOUND: Cannot find module 'sigstore'. Node's bundled npm is already new enough.
Manual dispatch
The workflow can be triggered manually from the Actions tab.
