SuperAPI AI Directory

conventional-changelog

General & other

View repository

Updated 10-15 21:05

About this skill

Conventional Changelog

A guide to writing commit messages using the Conventional Commits convention and mechanically generating CHANGELOGs and semver tags from them. Choose tools across npm / cargo / python / rust / go so you don't have to write the CHANGELOG by hand.

When to use

  • Set up a release flow for a new repo
  • The commit messages in an existing repo are inconsistent, and you want to introduce CHANGELOG generation
  • You tried release-please but it doesn't fit / want to consider another tool
  • You are looking for a changelog tool that fits your language (Rust / Python / Go, etc.)
  • Tags and CHANGELOG tend to break under manual operation, and you want to automate them

Conventional Commits format

Format: <type>[optional scope]: <subject>

feat(api): add rate-limit middleware
fix(auth): reject expired tokens before DB lookup
docs: clarify OAuth flow diagram
chore: bump deps

List of types and semver impact

type semver bump Included in CHANGELOG
feat minor ✓ (Added / Features)
fix patch ✓ (Fixed)
perf patch ✓ (Changed)
refactor None × (default)
docs None ×
style None ×
test None ×
build None ×
ci None ×
chore None ×
revert Depends on the preceding item ✓

Breaking change(major bump)

Two ways: write BREAKING CHANGE: in the footer, or write !:

feat(api)!: replace REST endpoint with GraphQL

BREAKING CHANGE: /api/v1/users is removed. Use GraphQL Query.user instead.

Writing both is the most considerate (immediately clear from the title + details in the footer).

scope

Optional, but adding it makes the CHANGELOG easier to read. In a monorepo, use the package name as the scope:

feat(@pkg/auth): add oauth2 flow
feat(@pkg/billing): stripe migration

The scope also automatically determines the "target package for changes" in release-please (manifest mode).

subject

  • Start with a lowercase letter (no trailing period)
  • Imperative mood ("add X" / "fix Y", avoid "added" or "fixes")
  • Aim for 50 characters or fewer

body / footer

Separate with a blank line, then the body, and another blank line before the footer (Closes #123, Co-authored-by: ..., BREAKING CHANGE: ...)。

CHANGELOG.md format (Keep a Changelog)

keepachangelog.com It complies with

# Changelog

All notable changes to this project are documented here.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- foo bar

## [1.2.0] - 2026-04-19

### Added

- New `--json` output flag for `foo scan` ([#42](https://github.com/owner/repo/pull/42))

### Changed

- `parse_config` now accepts YAML in addition to TOML

### Fixed

- Crash when config file is empty ([#40](https://github.com/owner/repo/issues/40))

### Deprecated

- `--legacy-mode` flag. Use `--mode=legacy` instead.

### Removed

- `foo bar --old-flag` (deprecated in 1.0.0)

### Security

- Upgrade `openssl` to 3.2.0 to address CVE-2025-XXXXX

## [1.1.0] - 2026-03-01

...

[Unreleased]: https://github.com/owner/repo/compare/v1.2.0...HEAD
[1.2.0]: https://github.com/owner/repo/compare/v1.1.0...v1.2.0

6 sections:

  • Added: New features
  • Changed: Changes to existing functionality
  • Deprecated: Deprecations (still present)
  • Removed: Removed
  • Fixed: Bug fixes
  • Security: Vulnerability fixes

semver tag practices

  • tag は v<MAJOR>.<MINOR>.<PATCH> format (v1.2.3)。v prefix is the mainstream
  • pre-release は v1.0.0-beta.1 / v1.0.0-rc.1
  • git tag -a v1.2.3 -m "Release v1.2.3"(annotated tag)→ git push origin v1.2.3
  • For linking tagging and deployment, use GitHub Release + Actions on: release to set it up

Tool comparison

Tool Supported languages Approach CHANGELOG generation version bump Main advantages Weaknesses
release-please npm / python / rust / go / java / php / ruby Release PRs are created automatically ○ ○ (PR merge で tag) Monorepo support, made by Google and stable PR-based with a learning curve
changesets npm only changeset add Recorded manually in ○ ○ Monorepo + flexible (npm workspaces) Not commit-driven
conventional-changelog-cli Language-agnostic (command) Generate one release from a commit ○ × Lightweight, appends to existing CHANGELOG tag / bump are done separately and manually
standard-version (deprecated) npm commit → bump + CHANGELOG ○ ○ Simple Maintenance stopped, migration to release-please recommended
git-cliff Language-agnostic (built in Rust) Highly customizable via TOML configuration ○ × Fast, flexible templates, favored in the Rust ecosystem tag / bump are separate
towncrier Python Write a news fragment for each PR ○ × Avoid merge-time conflicts with fragments Unique workflow
auto (intuit/auto) Multiple Label-based + CI integration ○ ○ GitHub label-driven Custom label conventions

Recommendation in one line

  • Want to automate with a single npm package → release-please
  • npm monorepo → release-please (manifest mode) or changesets (fine-grained control)
  • Rust → git-cliff
  • Python → towncrier(or release-please)
  • Go / general purpose → git-cliff or release-please
  • Want to generate only the CHANGELOG (tag management is manual) → conventional-changelog-cli or git-cliff

release-please setup (most recommended path)

When introducing it to a new npm project:

// release-please-config.json
{
  "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
  "release-type": "node",
  "packages": {
    ".": { "package-name": "my-pkg", "changelog-path": "CHANGELOG.md" }
  },
  "include-v-in-tag": true,
  "bump-minor-pre-major": true
}
// .release-please-manifest.json
{ ".": "0.0.0" }
# .github/workflows/release-please.yml
name: release-please
on:
  push:
    branches: [main]
permissions:
  contents: write
  pull-requests: write
jobs:
  release-please:
    runs-on: ubuntu-latest
    steps:
      - uses: googleapis/release-please-action@v4
        with:
          config-file: release-please-config.json
          manifest-file: .release-please-manifest.json

When you write commits following the conventional convention and push them to main, a "Release PR" is created automatically. When it is merged, tag + GitHub Release + CHANGELOG updates run.

Two forms of the publish-linked workflow:

Form trigger Advantages Drawbacks
Split jobs within the same workflow (needs: release-please + if: outputs.release_created) push: main Complete in one file, can reference release-please outputs directly, easy to grasp at a glance If the release-please job fails, publish is dragged down with it; responsibilities get mixed
Separate workflow (on: release) release: { types: [published] } Separation of concerns, publish also runs for manual releases, re-runs are independent Must write secrets / permissions in two places, cannot use release-please outputs

Selection guidelines: If it is a single package and a single publish destination (npm only), the same workflow is simpler. If there are multiple publish destinations (such as npm + GitHub Container Registry) or manual releases are also allowed, use on: release separate workflows. If you use OIDC Trusted Publishing, id-token: write you only need to grant permissions on the publish job side, and either approach works.

For details, see npm-release the publish flow in the skill (locally managed).

Staged releases for Pre-release (beta / rc)

When you want to release v2.0.0 containing a breaking change in the order beta → rc → official. Use release-please's prerelease / prerelease-type flag:

// release-please-config.json
{
  "packages": {
    ".": {
      "package-name": "my-pkg",
      "prerelease": true,
      "prerelease-type": "beta"
    }
  }
}

Flow:

  1. prerelease-type: "beta" accumulate commits with v2.0.0-beta.1 is cut
  2. with additional commits v2.0.0-beta.2 / beta.3 ... accumulate
  3. When promoting to RC: change the config to "prerelease-type": "rc" rewrite to → v2.0.0-rc.1
  4. At official release: "prerelease": false(or delete the field) → v2.0.0

How to write the CHANGELOG: release-please は Finalize an independent section for each beta/rc (this is not the Keep a Changelog [Unreleased] method of accumulating in one place).[2.0.0-beta.1] / [2.0.0-beta.2] / [2.0.0-rc.1] / [2.0.0] all remain.2.0.0 In the official version's section, all within the period feat! / BREAKING CHANGE are aggregated.

Monorepo + workspace:* interaction of

pnpm workspace で @org/core, @org/ui, @org/cli has, and@org/ui と @org/cli が @org/core when depending on:

// release-please-config.json
{
  "plugins": [
    { "type": "node-workspace", "updatePeerDependencies": true }
  ],
  "packages": {
    "packages/core": {
      "release-type": "node",
      "package-name": "@org/core",
      "include-component-in-tag": true
    },
    "packages/ui": {
      "release-type": "node",
      "package-name": "@org/ui",
      "include-component-in-tag": true
    },
    "packages/cli": {
      "release-type": "node",
      "package-name": "@org/cli",
      "include-component-in-tag": true
    }
  },
  "separate-pull-requests": false,
  "include-v-in-tag": true
}
  • include-component-in-tag: true → the tag name is @org/core-v2.0.0 includes the component name as in (monorepo collision avoidance)
  • node-workspace plugin → @org/core detects the bump on the side and @org/ui / @org/cli の dependencies."@org/core" rewrites, and adds both to the PR as a patch bump
  • updatePeerDependencies: true → peer dependencies are rewritten similarly (needed on the library side)
  • separate-pull-requests: false → aggregate the bumps of all packages into a single Release PR

package name in the commit scopeoperation of including:

feat(@org/core): add streaming parser
fix(@org/ui): button focus ring on safari
feat(@org/cli)!: rename --config to --profile

automatically determines the change target from the scope. An entry is written only to the CHANGELOG of the corresponding package.

workspace:* replacement mechanism: @org/ui/package.json で "@org/core": "workspace:*" even if written as, pnpm automatically replaces it at publish time with ^<実バージョン> . On the release-please side, at bump time workspace:* is left as is and the source is not modified. With this premise, things line up exactly (on the pnpm side, workspace:^ → ^同major、workspace:~ → ~同minor)。

git-cliff setup (Rust / general-purpose)

cargo install git-cliff
# or
brew install git-cliff
# cliff.toml
[changelog]
header = "# Changelog\n"
body = """
{% if version %}## [{{ version }}] - {{ timestamp | date(format="%Y-%m-%d") }}{% endif %}
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | upper_first }}
{% for commit in commits %}
- {{ commit.message | upper_first }} ({{ commit.id | truncate(length=7, end="") }})
{% endfor %}
{% endfor %}
"""

[git]
conventional_commits = true
filter_unconventional = true       # 既存の規約違反 commit を CHANGELOG から除外
protect_breaking_commits = true
commit_parsers = [
  { message = "^feat",     group = "Added" },
  { message = "^fix",      group = "Fixed" },
  { message = "^perf",     group = "Changed" },
  { message = "^refactor", group = "Changed" },
  { message = "^deprecat", group = "Deprecated" },
  { message = "^remove",   group = "Removed" },
  { message = "^security", group = "Security" },
  { message = "^revert",   group = "Reverted" },
  { message = "^(docs|style|test|build|ci|chore)", skip = true },
  { message = ".*",        skip = true },   # 規約違反一括除外(遡及書き換えしない)
]
tag_pattern = "v[0-9]*"
sort_commits = "newest"

In Rust-oriented operations, when used together with cargo-release ,cargo-release が Cargo.toml handles the version bump and tag creation, and git-cliff focuses solely on CHANGELOG generation (loose coupling). This is also the reason for not introducing PR-based automatic bumping like release-please.

git-cliff --output CHANGELOG.md            # 全履歴から再生成
git-cliff --unreleased --prepend CHANGELOG.md  # 未リリース分だけ上に追記
git-cliff --tag v1.2.0 --output CHANGELOG.md   # 特定 tag 向けに生成

Wiring with cargo-release (concrete example)

Cargo.toml の [package.metadata.release] set up a hook in.cargo release <level> When you run, bump → CHANGELOG generation → commit → tag → push run in sequence:

# Cargo.toml
[package.metadata.release]
# version bump 後・commit 前に git-cliff で CHANGELOG を更新
pre-release-hook = ["git", "cliff", "--tag", "v{{version}}", "--unreleased", "--prepend", "CHANGELOG.md"]
# tag は cargo-release 側が打つ
tag-name = "v{{version}}"
# release commit に CHANGELOG.md を含める
pre-release-commit-message = "chore(release): v{{version}}"

Order:

  1. cargo release minor --execute → Cargo.toml の version bump
  2. pre-release-hook で git cliff is finalized as the next version's [Unreleased] を [v0.2.0] - 2026-04-19 and CHANGELOG.md is updated
  3. cargo-release が Cargo.toml + CHANGELOG.md are combined into a release commit
  4. tag-name で v0.2.0 is created as an annotated tag
  5. cargo publish + git push --tags

Note: git-cliff side's [changelog.bump] section (features_always_bump_minor etc.) is a unique feature where git-cliff itself decides the bump, and should not be used when used together with cargo-release (the main body of bumping is cargo-release).<level> (patch / minor / major) is premised on being chosen by a human.

conventional-changelog-cli (lightweight, language-agnostic)

npm install -g conventional-changelog-cli
conventional-changelog -p angular -i CHANGELOG.md -s -r 0
# -p: preset (angular / atom / ember / eslint / jquery / jshint)
# -i: input, -s: same file, -r 0: 全履歴

The preset is mainly angular(the origin of conventional commits). It is suited to operations that append to an existing CHANGELOG.

Validate commit messages with pre-commit

Reject conventional convention violations at commit time:

commitlint

npm install -D @commitlint/cli @commitlint/config-conventional
// commitlint.config.js
module.exports = { extends: ['@commitlint/config-conventional'] };
# .pre-commit-config.yaml (prek / pre-commit)
- repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook
  rev: v9.16.0
  hooks:
    - id: commitlint
      stages: [commit-msg]
      additional_dependencies: ['@commitlint/config-conventional']

Pitfall: additional_dependencies If you do not add, at hook execution time config-conventional cannot be resolved and Cannot find module it fails with.commitlint.config.js で extends Just doing is insufficient; it must also be made explicit in the hook's isolated environment.

prek vs pre-commit: prek is a Rust-made pre-commit-compatible implementation (https://github.com/j178/prek)。設定ファイル (.pre-commit-config.yaml) and the hook specification is common, with execution speed 5-10x faster. The command is prek install / prek run. In the mizchi environment, prek is used (see the chezmoi-management skill).

commitizen (interactive commit creation)

npm install -D commitizen cz-conventional-changelog
# package.json
"config": { "commitizen": { "path": "./node_modules/cz-conventional-changelog" } }
# 使用
git cz

Common failures

  • Introducing commit conventions midway: Existing history stays in its broken format. release-please ignores "non-conforming commits" during scan. Retroactive rewriting is unnecessary; just get it right from next time on
  • chore: is all that's used, so no release PR is created: even though it's a bug fix, chore: written this way means it won't appear in the CHANGELOG / the version won't be bumped either.Choose the type by "kind of change", not by importance(fix is a user-visible fix, chore is internal cleanup)
  • Writing BREAKING CHANGE in the subject and omitting the footer: release-please は footer の BREAKING CHANGE: か type の ! is checked. Even if you write "breaking: ..." in the subject, it won't trigger a major bump
  • Manually editing the CHANGELOG + using the tool together: If you manually edit a CHANGELOG managed by release-please, it will drift on the next generation. Always add entries via a PR (use release-please's "Edit" feature)
  • Tagging first and then creating a PR with release-please: release-please tags at PR merge time. This conflicts with manual tagging. If operating manually, standardize on conventional-changelog-cli / git-cliff + manual tagging

Red flags

Thoughts that come up Reality
"The CHANGELOG is more carefully written by hand" Generating from commits means nothing is missed. Handwriting always causes omissions and inconsistent notation
"Tag first, then write the changelog" The order is reversed. Follow commit → tool generates CHANGELOG → tag
"Breaking changes are understood if written in the subject" tool は footer / ! is read mechanically. Natural-language text in the subject is ignored
"Let's add project-specific types" feat / fix / chore can be expressed within the scope of. Custom types are not supported by the tool's preset
"Fix conventional-violating commits with rebase" Retroactive rewriting pollutes history. Violations are fine as long as you get them right from next time on; the tool ignores them

Related