You know the drill. You bump package.json, update the CHANGELOG, run the tests, push the tag — and then someone opens a bug report six hours later because the latest entry in CHANGELOG.md still says 1.3.1.
It's not a bug in your code. It's a release metadata mismatch. And it happens way more often than it should.
Why existing tools don't fully cover this
-
semantic-releaseis great if you let a bot own your entire release workflow. For solo projects or teams who prefer manual control, it's overkill — and it owns your tag format too. -
standard-versionis deprecated. - There's no lightweight way to just check that three things agree: your manifest version, your changelog's latest entry, and your latest git tag.
So after the third time I shipped a mismatch, I built verscan.
What it does
Run it before every release (or add it to CI):
$ verscan
✓ package.json 1.4.0 (reference)
✓ CHANGELOG.md 1.4.0
✓ git tag 1.4.0
verscan: all sources match → 1.4.0
It checks three sources in one shot:
-
package.jsonversion (orpyproject.toml— auto-detected) -
Latest
## [x.y.z]heading in yourCHANGELOG.md -
Latest git tag via
git describe --tags --abbrev=0
When something disagrees, it tells you exactly what:
$ verscan
✓ package.json 1.4.0 (reference)
! CHANGELOG.md [no version heading found in CHANGELOG.md]
✓ git tag 1.4.0
verscan: 1 source(s) could not be read
Exit codes: 0 all match · 1 mismatch · 2 parse/read error — CI-friendly by design.
Install (zero dependencies, dual Node + Python)
# Node — no install needed
npx verscan
# Python
pip install verscan
verscan
Both versions produce identical output. I wrote them that way intentionally — if your team spans both toolchains, either version drops into CI without behavioral differences.
Flags
verscan --no-git # skip git tag check (useful before you've tagged)
verscan --no-changelog # skip CHANGELOG (for projects without one)
verscan --json # machine-readable output
verscan ./packages/ui # check a sub-package
Drop it in CI or a pre-push hook
# GitHub Actions
- name: Verify versions aligned
run: npx verscan
# pre-push hook
echo 'verscan' >> .git/hooks/pre-push
chmod +x .git/hooks/pre-push
Two design decisions worth noting
Why regex instead of a TOML parser for pyproject.toml?
tomllib is only available in Python 3.11+. To support Python >= 3.8 with zero dependencies, I use a targeted regex: r'^\[project\][^[]*?\bversion\s*=\s*["\']([^"\']+)["\']' with MULTILINE|DOTALL. It's deliberately conservative — it only matches version inside [project], not [tool.poetry] or anywhere else.
Why three sources instead of two?
The most common mismatch I've seen isn't the manifest vs. the tag — it's the CHANGELOG vs. everything else. Bumping package.json is often automated; updating the changelog is manual. Three-way verification catches both the "forgot to tag" and "forgot to update CHANGELOG" cases in one pass.
Links
- npm: https://www.npmjs.com/package/verscan
- PyPI: https://pypi.org/project/verscan/
- GitHub (Node): https://github.com/jjdoor/verscan
- GitHub (Python): https://github.com/jjdoor/verscan-py
What do you check before shipping a release? Do you have a checklist, a script, or do you rely on memory? Curious what others have landed on.
Top comments (0)