An upgrade breaks a build in an air-gapped environment, and the only thing the engineer has is the installed tree. The changelog is on a website they cannot reach. So are the release notes. The package they can read carries a README written for a version from two years ago.
What you get
You will end up with the changelog inside the published artifact. A check then fails the release when the file is absent, or when its newest entry is not the version being published. This is for you if you publish a library and want an upgrade to be diagnosable offline.
Short answer
List the changelog in the package’s file allowlist so it lands in the installed tree, and add a check that compares its newest version heading with the version being published. A changelog that lives only on a website is unreachable from a machine that has the package and no network, which is where most upgrade questions are asked.
You will need
Node 22 or later, and a package you publish. The allowlist is the files field. A handful of entries such as the manifest and the README ship whether or not you list them, which is what makes a missing changelog easy to overlook.1 The Keep a Changelog format gives the version headings the check below reads.2
Approaches compared
| Approach | When it fits | What it costs you | When to pick something else |
|---|---|---|---|
| A changelog file in the package | Any published library, because it travels with the code | Bytes in every install, and a file the release has to keep current | The history is genuinely uninteresting to consumers |
| A docs site section | You want the history rendered, linked and searchable | A network and a browser, neither of which an upgrade debugging session has | The reader is offline or working from an installed tree |
| Generated notes in the registry description | You already generate notes and want them where people browse | A page per version, and no way to read two versions side by side | Somebody is comparing several versions at once |
| Release notes on the forge only | Announcements, where a formatted page and a subscription list help | Nothing in the artifact, so an offline reader has no history at all | The package can be installed without visiting the forge |
These are not exclusive and the ordering is the point. Ship the file, then render it wherever else you like. Of the several renderings built from one source, the one in the artifact is the one that cannot go missing. The file in the package is the copy that is guaranteed to exist alongside the code that is actually installed, and every other copy is a convenience built from it.
The version check is what keeps the file accurate over time. A changelog nobody verifies drifts from the releases within a few months. The usual cause is a patch that went out without an entry. A reader who notices that once stops trusting the file entirely.
Put it in the allowlist
The allowlist is a decision to include, and anything missing from it is silently absent.
{
"name": "@meterco/sdk",
"version": "1.5.0",
"description": "Client for the Meters API",
"files": ["src", "CHANGELOG.md"],
"repository": { "type": "git", "url": "https://github.com/meterco/sdk-js" },
"main": "src/index.js"
}
Keep the repository field too. The shipped changelog is the newest entries and the repository is the whole history, so a reader who needs more than the file has somewhere to go.
Check what ships rather than what you meant to ship. The allowlist interacts with ignore files and with defaults, and the only reliable answer is to resolve it the way a publish does.
Compare the newest entry with the version
One comparison, run before the publish.
const top = /^##\s+v?(\d+\.\d+\.\d+)/m.exec(text)
if (!top) problems.push('the changelog has no version heading')
else if (top[1] !== manifest.version) {
problems.push(`the changelog's newest entry is ${top[1]}, the package is ${manifest.version}`)
}
Run it in the release job, before the publish rather than after. A published version cannot be replaced, so a changelog that lags is a defect that stays in the registry forever.3
The check also catches the opposite mistake: a changelog updated for a version nobody released, which happens when a release is abandoned halfway and the entry is left behind.
Check it worked
Audit a package that does this correctly and one that does not.
node demo.mjs
package/
ships CHANGELOG.md, README.md, package.json, src/index.js, src/meters.js
newest 1.5.0
problems none
stale-package/
ships package.json, src/index.js
newest -
problems no CHANGELOG ships with the package
The second package has a changelog on disk. It is not in the allowlist, so it never reaches anyone who installs the package, even though nothing about the repository looks wrong. That is the failure this check exists to catch, and it is invisible in every view except the published artifact.
The file list is worth reading on its own. Tests are excluded here, which is deliberate. A package that ships its whole tree by accident carries fixtures, coverage output, and whatever else was in the directory at publish time. That is bytes on every install and, sometimes, a credential.
node --test willship.test.mjs
1..6
# tests 6
# suites 0
# pass 6
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 128.384339
When it goes wrong
The changelog ships and is empty. A generator wrote it into a path the allowlist does not cover, so the shipped copy is an older file with the same name. Check the shipped bytes, not the repository. Installing the published package into a temporary directory once per release is the crudest version of this check and it catches most of it.
The check fails on a pre-release. The version carries a suffix the heading pattern does not match. Decide how pre-releases appear in the changelog and make the pattern match that. Skipping the check on a pre-release is the other defensible answer, as long as it is a decision rather than an accident.
The file grows to thousands of lines. Every release since the beginning is in one file. Keep the recent entries in the shipped file and archive the rest, with a line saying where they went. A file that stops at the last major version, plus a link to the rest, reads better than four years of history nobody scrolls through.
Two packages in one repository share a changelog. Each installs a file describing the other’s releases as well. Give every published package its own, generated from the commits that touched it.
When not to do this
Do not ship a changelog generated from every commit. A file listing four hundred dependency bumps is worse than no file, because a reader has to scan it before concluding it says nothing.
Do not ship it and stop maintaining the rendered version. The two copies are the same content and the same source, and letting the website drift teaches people not to trust either.
Do not treat the shipped file as the announcement. It answers what changed for somebody already holding the package, and telling people a release exists is a different job, with a different audience and a different cadence.
Related how-tos
Last verified
Verified 2026-09-14 against Node 22.22.2. Both output blocks are what the preceding command printed.
Footnotes
-
The npm documentation lists five things that ship regardless of the allowlist: the manifest, a README, a LICENSE or LICENCE, the file named in
mainand the files named inbin. It then lists seventeen that are ignored by default, among themCVS,.svn,.hgand.wafpickle-N, names that date the list. Six of the seventeen cannot be included even by naming them. A changelog appears in neither list, which is the whole reason for this page. ↩︎ Back to text -
Keep a Changelog opens with the line that nobody should let their friends dump git logs into changelogs. It then keeps its own changelog, in its own format, at the foot of the page. Version 1.1.0 is dated 2019-02-15, in the ISO 8601 form the format prescribes. The
Unreleasedsection arrived in 0.0.5, on 2014-08-09, the same day as 0.0.4. The format is MIT licensed and offered in 28 languages. ↩︎ Back to text -
The rule belongs to the registry, not to the changelog. The guide to
npm unpublishallows a package to be removed within 72 hours of its first publish, and after that only under the conditions the policy sets out. Either way, a name and version pair is unique and cannot be reused by removing it and publishing again, so taking the version down does not give it back. The guide recommends a minor version update instead, which is a new entry in the changelog and a fresh chance to get it wrong. ↩︎ Back to text