# How to ship a changelog inside your published package

> Put the changelog in the published artifact, and check in CI that it ships and that its newest entry matches the version being released.

Source: https://voxgig.com/howto/ship-a-changelog-inside-the-package

- Audience: library-maintainer
- Level: beginner
- Languages: typescript, javascript
- Verified: 2026-09-14
- Published: 2026-09-14
- Updated: 2026-09-24

## 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](https://docs.npmjs.com/cli/v10/configuring-npm/package-json#files). 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](https://keepachangelog.com/en/1.1.0/) 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](https://keepachangelog.com/en/1.1.0/) | 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](https://docs.astro.build/en/guides/markdown-content/) | 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](https://docs.npmjs.com/about-package-readme-files) | 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](https://docs.github.com/en/repositories/releasing-projects-on-github/about-releases) | 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.

```json title="package/package.json"
{
  "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.

```js title="willship.mjs"
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.

```bash
node demo.mjs
```

```text output
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.

```bash
node --test willship.test.mjs
```

```text output
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

- [Generate a changelog from conventional commits](/howto/changelog-from-conventional-commits)

- [Judge whether an unofficial SDK is safe to depend on](/howto/judge-an-unofficial-sdk-before-adoption)

## Last verified

Verified 2026-09-14 against Node 22.22.2. Both output blocks are what the preceding command printed.

[^1]: The [npm documentation](https://docs.npmjs.com/cli/v10/configuring-npm/package-json#files)
lists five things that ship regardless of the allowlist: the manifest, a README, a LICENSE or
LICENCE, the file named in `main` and the files named in `bin`. It then lists seventeen that are
ignored by default, among them `CVS`, `.svn`, `.hg` and `.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.

[^2]: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) 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 `Unreleased` section 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.

[^3]: The rule belongs to the registry, not to the changelog. The
[guide to `npm unpublish`](https://docs.npmjs.com/unpublishing-packages-from-the-registry)
allows a package to be removed within 72 hours of its first publish, and after that only under
the conditions the [policy](https://docs.npmjs.com/policies/unpublish) 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.