LogPulse / guides

How to keep a changelog for an npm package

npm installs your package; it doesn't tell anyone what changed. That job falls to your CHANGELOG.md, your GitHub releases and two registry features most maintainers underuse: dist-tags and deprecation messages.

Updated Last checked 5 min readOpen-source libraries and packages

The short answer

  • Keep CHANGELOG.md in the repository and link it, or your GitHub releases, from the README: the README is what the npm package page shows.
  • npm version updates package.json and, in a git repository, creates a version commit and tag. Use the same version in the changelog heading.
  • Publish prereleases with npm publish --tag next so latest doesn't move.
  • Use npm deprecate to warn people away from broken or unsupported versions, and record it in the changelog.
  • Changes to engines, peerDependencies, exports, module format and types are often the real breaking changes.

Changelog entry for an npm package

A major release with the three breaking changes npm packages ship most often, in Keep a Changelog 2.0.0 format: each one marked **Breaking:** under its type. Delete the ones that don't apply.

CHANGELOG.md
## [3.0.0] - YYYY-MM-DD

Three breaking changes: Node.js 20 or later, ESM only, and imports limited
to the paths in `exports`.

### Added
- `my-lib/utils` entry point.

### Changed
- **Breaking:** Node.js 20 or later is required (`engines.node` is now `>=20`).
- **Breaking:** The package is ESM-only. If you load it with `require("my-lib")`,
  switch to `import { parse } from "my-lib"`, or stay on 2.x.
- **Breaking:** Only paths listed in `exports` can be imported. Deep imports such as
  `my-lib/dist/utils.js` now fail with ERR_PACKAGE_PATH_NOT_EXPORTED.
  Import from `my-lib/utils` instead.

### Deprecated
- `parseSync()`: use `parse()`. It will be removed in 4.0.0.

[3.0.0]: https://github.com/OWNER/REPO/compare/v2.4.1...v3.0.0

my-lib, its functions and the versions are placeholders.

Free tool · no sign-up

Can a coding agent read your changelog?

Paste your repository, a GitHub release or a CHANGELOG.md link. The free converter shows every version, breaking change, migration step and deprecation it can extract, as JSON plus an llms.txt summary.

No AI and nothing stored. Or paste a CHANGELOG.md instead

01Put the changelog where npm users will find it

The npm package page shows your README, not your changelog. Add a ‘Changelog’ link near the top of the README that points at CHANGELOG.md or your GitHub releases, and set the repository field in package.json so people can get from npm to your code in one click.

Keeping CHANGELOG.md in the repository means it's versioned with the code and reviewed in pull requests. Publishing each entry as a GitHub release as well notifies people who watch the repository. Keep a Changelog vs GitHub Releases explains how to do both without double work.

02Make versions, tags and headings agree

npm version patch, minor or major updates package.json and, in a git repository, creates a version commit and a v-prefixed tag. It refuses to run if the working tree has uncommitted changes, so commit the changelog entry first. Use the same version in the changelog heading and the GitHub release title so readers, and tools, can match them up.

A release, step by step
# 1. Move the Unreleased notes under "## [2.5.0] - YYYY-MM-DD" and commit
git commit -am "docs: changelog for 2.5.0"

# 2. Bump package.json, commit and tag (needs a clean working tree)
npm version minor        # 2.4.1 -> 2.5.0, creates the tag v2.5.0

# 3. Publish and push the commit and tag
npm publish
git push --follow-tags

In a monorepo, Changesets does this per package: changeset version bumps versions and writes each package's CHANGELOG.md, and changeset publish runs npm publish for every package with a new version.

03Publish prereleases without moving latest

By default npm publish tags the new version latest, which is what a plain npm install my-lib installs. Publish betas and release candidates under another dist-tag so nobody gets them by accident, and give each one its own changelog entry with the full prerelease version.

Prereleases with a dist-tag
# package.json "version": "3.0.0-beta.1"
npm publish --tag next

# People opt in explicitly
npm install my-lib@next

# When 3.0.0 is stable, publish it as usual and it becomes latest
npm publish

04Deprecate versions in the registry, not just in prose

npm deprecate attaches a message to a version or a range, and npm shows it as a warning to anyone who installs a matching version. Use it for versions with a serious bug, or majors you no longer support, and note it in your changelog.

npm deprecate
# Warn anyone installing an affected range
npm deprecate my-lib@"< 2.3.1" "Security fix in 2.3.1, please upgrade"

# Undo it: an empty message un-deprecates
npm deprecate my-lib@2.3.0 ""

For a deprecated function inside a version you still support, warn at runtime as well. Node's util.deprecate() wraps a function so it emits a deprecation warning the first time it's called. Then write the notice: how to write deprecation notices.

05Know what breaks npm users

Many breaking changes in npm packages live in package.json rather than in your code.

ChangeWho it breaksChangelog line
engines.node raisedProjects on older Node.js versions: npm warns, and refuses with engine-strict setRequires Node.js 20 or later.
peerDependencies narrowedApps outside the new range: npm reports a peer-dependency conflictRequires react ^18 or ^19.
ESM-only buildCommonJS code that loads the package with require()ESM-only: use import.
exports added or narrowedAnyone importing a path that isn't listed (ERR_PACKAGE_PATH_NOT_EXPORTED)Only my-lib and my-lib/utils can be imported.
Type definitions changedTypeScript users, even when runtime behavior is the sameOptions.timeout is now number (was number | string).

Under Semantic Versioning these belong in a major version, even when no function signature changed. More in semantic versioning and changelogs, and for how these changes look from your users' side, how to track breaking changes in npm dependencies.

Before and after

Too vague

3.0.0: modernized the build.

Specific and actionable

3.0.0 is ESM-only and requires Node.js 20+. Replace require("my-lib") with import. Deep imports like my-lib/dist/utils.js are no longer exported; import from my-lib/utils instead.

Fictional example: use your own verified names, versions and behavior.

LogPulse

Generate release notes with LogPulse

LogPulse is a hosted changelog that people and coding agents can both read. Here's exactly what it can do for the steps above, and what it can't.

  1. Check your changelog, free

    Paste a CHANGELOG.md or a GitHub releases link. A deterministic parser (no AI, nothing stored) shows the versions, breaking changes, migration steps and deprecations an agent can extract.

    Open the converter

  2. Draft notes from GitHub

    Connect a repository: merged pull requests, commits to your default branch and published GitHub releases collect in an inbox. AI drafts a note in the tone you pick, listing breaking changes and deprecations only when the changes clearly show them. Nothing is published until you approve it. 3 AI drafts on Free; unlimited on Open source and Pro. Pro opens soon: .

    Free for open source

  3. Publish once, read everywhere

    Approved notes go to a hosted changelog and an in-app widget, to email subscribers on Open source and Pro (Pro opens soon), and to a JSON feed and MCP server coding agents can query. CI and agents can publish with an API key.

    Read the docs

What it doesn't do: LogPulse doesn't tag releases, bump versions, publish to npm or PyPI, or write CHANGELOG.md back to your repository; keep your release tooling for those. AI drafts can miss context, so review them like a pull request.

Before you publish

  • The README links to the changelog
  • The repository field is set in package.json
  • The changelog heading, git tag and npm version match
  • Prereleases are published with --tag
  • Broken or unsupported versions are flagged with npm deprecate
  • engines, peerDependencies, exports and type changes are listed as breaking where they are

Common questions

Does npm show my CHANGELOG.md?

Not on the package page; it shows the README. Link the changelog from the README. Whether CHANGELOG.md is inside the published package depends on your files field and .npmignore; check with npm pack --dry-run.

Should I unpublish a broken version?

Usually not. Deprecate it and publish a fix. npm restricts unpublishing, and removing a version can break installs that pinned it, while a deprecation warns new installs without breaking existing lockfiles.

How do I list a prerelease in the changelog?

Use the full prerelease version in the heading, for example ## [3.0.0-beta.1] - YYYY-MM-DD, and mark the GitHub release as a pre-release. When 3.0.0 ships, its entry can summarise everything since 2.x so stable users don't have to read every beta.

Are TypeScript type changes breaking?

If code that compiled before fails to compile after the upgrade, treat it as breaking for TypeScript users and list it, even if the JavaScript behaves the same.

Sources

Facts last checked against these sources on .

More guides: open-source libraries and packages

All release notes guides

Tools and resources

LogPulse

Changelogs coding agents can upgrade from.

Publish versions, breaking changes, migration steps and deprecations once. People get a widget and hosted changelog; agents get a JSON feed and MCP server. Free for open source.