changelog-from-commits

A CLI that turns your git history into a polished CHANGELOG.md, following Conventional Commits. It reads every commit since your last tag, groups them by type, links each entry to its PR, and prepends the new release section — leaving your header prose intact.

zero runtime dependencies Node >= 18 MIT CJS + ESM + .d.ts 304 tests · 97% stmts
npx changelog-from-commits

No config needed. Add --dry-run to preview it without writing a file.

Live transformer

Commits in, changelog out.

Edit the commit subjects on the left. The right pane runs the package's real header grammar and section ordering, and re-renders as you type. The toggles map to actual CLI flags — so playing here teaches you the tool.

git log --pretty=%s 8 commits
one subject per line
CHANGELOG.md markdown
Rendered changelog markdown

Simulated against a repository at acme/widgets with a previous tag of v1.3.0. Real runs resolve that with git describe --tags --abbrev=0.

What it does

Opinionated where it matters, quiet everywhere else.

Every behaviour below is a default. There is no required config file.

Zero runtime dependencies

git log is invoked directly through child_process with a delimited --pretty format. Nothing to audit, nothing to bloat your lockfile.

Breaking changes, up top

Detected from a ! after the type or a BREAKING CHANGE: footer. Rendered in a prominent ⚠ BREAKING CHANGES section using the footer text — that's where the migration note lives.

PR links, commit fallback

The repo URL is read from your origin remote — scp-style, ssh:// and https:// all parse. GitHub, GitLab and Bitbucket URL shapes are built correctly; anything else falls back to the GitHub shape.

Monorepo aware

--tag-prefix web-v scopes the range to that package's tags; --path packages/web scopes the commits. Repeat --path for several directories.

Reverts pair off

A commit and its revert inside the same range both disappear — the feature never shipped. Matching prefers the This reverts commit <sha> line and falls back to the subject.

Dual CJS + ESM builds

Ships CommonJS, ESM and TypeScript declarations. Import generateChangelog, or the lower-level parseCommit / groupCommits / applyReverts to render your own format.

Config only if you want it

Searched upward: changelog.config.js|.mjs|.cjs|.json, .changelogrc.*. Override the whole type table. CLI flags always beat config values.

Dry run, and safe writes

--dry-run prints to stdout and touches nothing — flag-only by design. Writes splice the new section after your preamble and before the newest release, so header prose survives.

Backfill an existing repo

--all rebuilds the whole file, one section per tag, so adopting the tool on a repo with 30 releases is one command instead of 30. It replaces the file, so it refuses to clobber a non-empty changelog without --force.

Gitmoji and loose spellings

A leading or :sparkles: is stripped before parsing, so a gitmoji repo gets a real changelog instead of an empty one. feature: folds into feat:, bugfix: into fix: — no duplicate sections.

Betas don't swallow commits

Tag v1.1.0-beta.1, then release 1.1.0, and the changelog still covers everything that went into the beta — a stable release starts from the last stable tag. A prerelease target starts from the previous prerelease instead.

The footgun

--tag-prefix and --path do two different jobs.

In a monorepo you almost always want both. The prefix picks the starting tag. The path picks the commits. Use one without the other and your package's changelog quietly fills up with someone else's work.

Prefix only — leaky
npx changelog-from-commits \
  --tag-prefix web-v \
  --output packages/web/CHANGELOG.md

The range starts at the last web-v* tag — correct. But every commit in that range is included:

### Features

* web: new settings panel
* api: add /v2/session     ← not this package
* docs-site: dark mode      ← not this package
Prefix + path — scoped
npx changelog-from-commits \
  --tag-prefix web-v \
  --path packages/web \
  --output packages/web/CHANGELOG.md

Same range, but only commits that touched packages/web survive:

### Features

* web: new settings panel
* web: remember last tab

(api and docs-site commits filtered out)

--tag-prefix web-v

Scopes the range. The changelog starts at the last web-v* tag rather than whatever tag happens to be newest in the repo.

--path packages/web

Scopes the commits. Only commits that touched that directory are included. Repeatable, so a package spanning two directories still works.

-o packages/web/CHANGELOG.md

Writes next to the package instead of at the repo root, so each package keeps its own file.

CLI reference

Every flag, and nothing else.

The arg parser is hand-rolled and knows every flag up front, so a typo is an error instead of a silently ignored argument. Select a flag to expand it.

Command line flags for changelog-from-commits
FlagSummary

--flag=value works everywhere --flag value does, and -- stops flag parsing. Exit codes are 0 on success and 1 on any error — including a duplicate version heading — so it composes inside a release script.

Configuration

Optional. Searched upward from your working directory.

changelog.config.js · .mjs · .cjs · .json, or .changelogrc.js|.mjs|.cjs|.json and bare .changelogrc. CLI flags always win over config values. --dry-run is flag-only by design — a config file that silently suppressed writes would be a trap.

// changelog.config.js
module.exports = {
  tagPrefix: 'v',
  output: 'CHANGELOG.md',
  includeAll: false,
  paths: [],
  pairReverts: true,
  linkReferences: true,
  otherTitle: 'Other Changes',
  types: [
    { type: 'feat', title: '🚀 Features' },
    { type: 'fix', title: '🐛 Bug Fixes' },
    { type: 'perf', title: '⚡ Performance' },
    { type: 'chore', title: 'Chores', hidden: true },
  ],
};

Settable keys: from, to, output, version, tagPrefix, includeAll, paths, pairReverts, types, repoUrl, linkReferences, otherTitle.

Quick start

Three commands from zero to a release section.

Requires Node 18 or newer, and git on your PATH.

Preview it

Prints the section to stdout and writes nothing, so you can run it anywhere without consequences.

npx changelog-from-commits --dry-run

Add it to the project

Keep it as a dev dependency once you're happy, so CI resolves the same version every time.

npm install --save-dev changelog-from-commits

Wire it into release

Bump the version first so the heading picks it up from package.json, then generate.

{
  "scripts": {
    "release": "npm version minor && changelog-from-commits --tag-prefix v"
  }
}

FAQ

Common questions.

Short answers to what people ask most about generating changelogs from git history.

What is changelog-from-commits?

changelog-from-commits is a command-line tool that reads your git history and writes a formatted CHANGELOG.md. It parses commit messages that follow the Conventional Commits specification, groups them by type into sections like Features and Bug Fixes, and links every entry to its pull request or commit. It has zero runtime dependencies and needs no configuration file.

How do I generate a CHANGELOG.md from my git commits?

Run npx changelog-from-commits inside any git repository. It reads every commit since your last git tag, groups them by conventional type, and prepends a new release section to CHANGELOG.md. Add --dry-run to print the result to stdout without writing a file.

Does it require a config file?

No. Every behaviour is a default and the tool is designed to work with no configuration at all. A config file is optional — changelog.config.js, .changelogrc.json and similar names are searched for upward from the working directory — and CLI flags always take precedence over config values.

How does it handle breaking changes?

Breaking changes are detected two ways: a ! after the type or scope (feat(api)!: drop v1), or a BREAKING CHANGE: footer in the commit body. They are rendered in a prominent ⚠ BREAKING CHANGES section at the top of the release, using the footer text where present, since that is where the migration note lives. The commit also still appears under its own type section.

Does it work in a monorepo?

Yes, using two flags together. --tag-prefix web-v scopes the commit range to that package's tags, and --path packages/web scopes which commits are included. You usually want both: with only the tag prefix, a package's changelog will list every other package's commits since that tag.

Can I generate a changelog for a repository that already has releases?

Yes. changelog-from-commits --all rebuilds the entire file, emitting one section per git tag plus an Unreleased section for anything after the newest tag. Because it replaces the file rather than prepending to it, it refuses to overwrite a non-empty changelog unless you also pass --force.

Does it support gitmoji commit messages?

Yes. A leading emoji or :shortcode: — as in ✨ feat(auth): add OAuth — is stripped before parsing, so a repository that prefixes every commit still produces a complete changelog. The emoji is removed from the output by default; set keepEmoji: true in a config file to keep it.

Which git hosts are supported for commit and PR links?

The repository URL is detected from your origin remote, and scp-style (git@host:owner/repo.git), ssh:// and https:// remotes all parse. GitHub, GitLab and Bitbucket URL shapes are built correctly; any other host falls back to the GitHub shape, which most self-hosted forges use. Override with --repo-url, or disable links entirely with --no-links.

What happens to commits that do not follow Conventional Commits?

They are skipped by default, and the CLI reports how many it dropped so the omission is never silent. Pass --include-all to collect them into an "Other Changes" section instead. Merge commits are always dropped, since the commits they bring in are already listed.

Does it have any runtime dependencies?

None. git log is invoked directly through Node's child_process with a delimited --pretty format, and the argument parser is hand-rolled. Installing it adds exactly one package to your lockfile. It requires Node.js 18 or newer and ships CommonJS, ESM and TypeScript declarations.