Zero runtime dependencies
git log is invoked directly through child_process with a delimited --pretty format. Nothing to audit, nothing to bloat your lockfile.
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.
npx changelog-from-commits
No config needed. Add --dry-run to preview it without writing a file.
Live transformer
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.
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
Every behaviour below is a default. There is no required config file.
git log is invoked directly through child_process with a delimited --pretty format. Nothing to audit, nothing to bloat your lockfile.
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.
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.
--tag-prefix web-v scopes the range to that package's tags; --path packages/web scopes the commits. Repeat --path for several directories.
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.
Ships CommonJS, ESM and TypeScript declarations. Import generateChangelog, or the lower-level parseCommit / groupCommits / applyReverts to render your own format.
Searched upward: changelog.config.js|.mjs|.cjs|.json, .changelogrc.*. Override the whole type table. CLI flags always beat config values.
--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.
--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.
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.
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.
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
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)
Scopes the range. The changelog starts at the last web-v* tag rather than whatever tag happens to be newest in the repo.
Scopes the commits. Only commits that touched that directory are included. Repeatable, so a package spanning two directories still works.
Writes next to the package instead of at the repo root, so each package keeps its own file.
CLI reference
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.
| Flag | Summary |
|---|
--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
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 },
],
};
// changelog.config.mjs
// A plain object or a function both work.
export default () => ({
tagPrefix: 'v',
output: 'docs/CHANGELOG.md',
});
{
"tagPrefix": "web-v",
"output": "packages/web/CHANGELOG.md",
"paths": ["packages/web"],
"includeAll": false,
"pairReverts": true,
"linkReferences": true,
"otherTitle": "Other Changes"
}
Settable keys: from, to, output, version,
tagPrefix, includeAll, paths, pairReverts,
types, repoUrl, linkReferences, otherTitle.
Quick start
Requires Node 18 or newer, and git on your PATH.
Prints the section to stdout and writes nothing, so you can run it anywhere without consequences.
npx changelog-from-commits --dry-run
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
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
Short answers to what people ask most about generating changelogs from git history.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.