Commitizenによる統一されたGitコミットメッセージの管理

Commitizenの導入手順と使い方を解説。Conventional Commitsがsemantic-release等でどう解析されバージョン自動決定・チェンジログ生成に使われるかを、実際に構築したリポジトリでのcommitlint検証・チェンジログ生成の実行結果とともに、モノレポでのスコープ活用・CI強制・squashマージの落とし穴まで解説します。

チーム開発において、一貫性のあるコミットメッセージは、プロジェクトの履歴を理解しやすくし、変更履歴の自動生成やCI/CDのトリガーとしても役立ちます。Commitizen は、Gitのコミットメッセージを効果的に統一するためのツールで、対話的なプロンプトを通じてコミットメッセージの作成を支援します。

本記事では、Commitizenの導入・使い方に加えて、Conventional Commitsの構造化フォーマットが「なぜ」バージョン管理やチェンジログ生成を自動化できるのかという仕組み、実際に構築した検証用リポジトリでの実行結果、モノレポでのスコープ活用・CIでの強制・squashマージとの相互作用といった実務上の論点を扱います。

Commitizenとは

Commitizen は、コミットメッセージのフォーマットを強制し、開発者が一貫性のあるメッセージを作成できるようにするCLIツールです。対話形式でコミットメッセージの各要素(タイプ、スコープ、説明など)を入力できるため、手動でフォーマットを覚える必要がありません。

Conventional Commitsがバージョン自動決定を可能にする仕組み

Commitizenが生成するメッセージは Conventional Commits 規約に準拠しています。この規約の本質は、コミットメッセージを人間が読む自然文ではなく、機械が構文解析できる構造化データとして扱う点にあります。

<type>(<scope>): <description>

<body>

<footer>

semantic-releasestandard-version(後継は後述)は、このヘッダー行を正規表現でパースし、type フィールドと、本文・フッターにある BREAKING CHANGE: の有無、あるいはヘッダーの type(scope)!: のように ! が付いているかどうかを抽出します。次のバージョン番号は、リリース以降の全コミットのうち最も重大度の高い変更によって決定されます。

検出される要素semver への影響具体例
BREAKING CHANGE: フッター、または type!:MAJOR(X.0.0feat(devtoolbox)!: drop support for legacy tool ID format
feat:MINOR(0.X.0feat(blog): add tag-based CTA filter
fix:PATCH(0.0.Xfix(calcbox): correct rounding error in unit converter
docs:, style:, refactor:, test:, build:, ci:, chore:バージョン変更なしchore(scratch): add junk placeholder file

この判定フローを図にすると次のようになります。

Conventional CommitからSemantic Versionバンプへの決定フロー

semantic-release は内部的に @semantic-release/commit-analyzer プラグインでこの判定を行い、@semantic-release/release-notes-generator が同じコミット集合を type ごとにグルーピングして CHANGELOG.md を生成します。つまり、コミットメッセージのフォーマットを固定することで、「次のバージョン番号は何か」「リリースノートに何を書くか」という本来は人間の判断が必要な2つの作業を、パース可能な文字列処理だけで自動化できる、というのがConventional Commitsの核心的な価値です。

導入手順

1. Commitizenのインストール

まず、Commitizen CLIをグローバルにインストールします。

npm install -g commitizen

これにより、git cz コマンドが利用可能になります。

2. コミットメッセージ規約アダプターのインストール

Commitizenを使用する際、どのようなフォーマットでコミットメッセージを記述するかを定義する「アダプター」が必要です。ここでは、Conventional Commits 規約に準拠したコミットメッセージを作成できる cz-conventional-changelog を使用します。

npm install -g cz-conventional-changelog

次に、Commitizenがこのアダプターを使用するように設定ファイルを作成します。

# .czrc ファイルを作成し、アダプターのパスを記述
echo '{ "path": "cz-conventional-changelog" }' > ~/.czrc

注意: この .czrc ファイルはユーザーのホームディレクトリに作成されます。プロジェクトごとに異なるアダプターを使用したい場合は、プロジェクトのルートディレクトリに package.json を作成し、config.commitizen.path を設定する方法もあります。

3. commitlintでフォーマットを検証する

Commitizenは「正しいフォーマットを書きやすくする」ツールであって、「間違ったフォーマットを防ぐ」ツールではありません。git commit -m "適当なメッセージ" を直接打てば、規約を無視したコミットがそのまま入ってしまいます。これを機械的に拒否するには commitlint を組み合わせます。

npm install -D @commitlint/cli @commitlint/config-conventional
// commitlint.config.js
module.exports = { extends: ["@commitlint/config-conventional"] };

そして .git/hooks/commit-msg(実運用では Husky や本リポジトリで使っている Lefthook 経由で管理)に以下を仕込むと、フォーマット違反のコミットはローカルの git commit の時点で失敗するようになります。

#!/bin/sh
npx --no-install commitlint --edit "$1"

使用方法

通常の git commit コマンドの代わりに、git cz コマンドを使用してコミットメッセージを作成します。

git cz

このコマンドを実行すると、対話型のプロンプトが表示され、コミットメッセージの各部分を選択・入力することができます。

cz-cli@4.3.1, cz-conventional-changelog@3.3.0

? Select the type of change that you're committing: (Use arrow keys)
❯ feat:     A new feature
  fix:      A bug fix
  docs:     Documentation only changes
  style:    Changes that do not affect the meaning of the code (white-space,
formatting, missing semi-colons, etc)
  refactor: A code change that neither fixes a bug nor adds a feature
  perf:     A code change that improves performance

プロンプトの指示に従って、コミットのタイプ(feat, fix, docs など)、スコープ、短い説明、詳細な説明などを入力していくことで、Conventional Commits規約に沿ったコミットメッセージが自動的に生成されます。

Conventional Commitsの主なタイプ

  • feat: 新機能の追加
  • fix: バグ修正
  • docs: ドキュメントのみの変更
  • style: コードのスタイルに関する変更(フォーマット、セミコロンなど)
  • refactor: リファクタリング(バグ修正でも新機能でもないコード変更)
  • perf: パフォーマンス改善
  • test: テストコードの追加や修正
  • build: ビルドシステムや外部依存に関する変更
  • ci: CI/CDに関する変更
  • chore: その他の雑多な変更(ビルドプロセスや補助ツールなど)
  • revert: 以前のコミットの取り消し

実際に動かして確認する

「Conventional Commitsで自動化できる」という説明だけでは実感が湧きにくいため、/private/tmp 以下の独立したGitリポジトリ(このブログのリポジトリとは無関係)を1つ作り、commitizen / cz-conventional-changelog / @commitlint/cli / conventional-changelog-cli を実際にインストールして、一連の流れを最初から最後まで動かしてみます。

対話型 git cz を実際に実行する

expect で対話プロンプトへのキー入力(矢印キーでの選択・テキスト入力・Enter)を自動化し、実際に ./node_modules/.bin/git-cz を起動して1コミットを作成した、実際の実行ログ(制御文字を除去したもの)が以下です。

$ ./node_modules/.bin/git-cz
cz-cli@4.3.1, cz-conventional-changelog@3.3.0

? Select the type of change that you're committing:
  feat:     A new feature

? What is the scope of this change (e.g. component or file name): (press enter to skip)
blog

? Write a short, imperative tense description of the change (max 88 chars):
add tag-based CTA filter for affiliate cards

? Provide a longer description of the change: (press enter to skip)


? Are there any breaking changes? No

? Does this change affect any open issues? No

[main 6e71014] feat(blog): add tag-based CTA filter for affiliate cards
 1 file changed, 3 insertions(+)
 create mode 100644 src.js

同じ要領で fix(calcbox): ... コミットと、破壊的変更を示す feat(devtoolbox)!: ...BREAKING CHANGE: フッター付き)、規約を守らない汎用コミットも含めて積み上げた、実際の git log の出力が次の通りです。

$ git log --oneline
cdcd80a chore(scratch): add junk placeholder file
7d77cd8 feat(devtoolbox)!: drop support for legacy tool ID format
cab1887 fix(calcbox): correct rounding error in unit converter
6e71014 feat(blog): add tag-based CTA filter for affiliate cards
dc68d2a feat: initial project scaffold with commitizen and commitlint config

feat(devtoolbox)!: コミットの本文には、実際に次のフッターが入っています。

$ git log -1 --format='%H%n%s%n%n%b' 7d77cd8
7d77cd88705ce25739f93e87e17fa38663f9ce01
feat(devtoolbox)!: drop support for legacy tool ID format

BREAKING CHANGE: tool IDs must now be kebab-case; old camelCase IDs
registered before v2 will 404. Run the migration script in
scripts/migrate-tool-ids.js before upgrading.

commitlintが実際にメッセージを拒否・受理する様子

commitlint に標準入力でメッセージを渡すと、規約違反のメッセージは実際にエラー終了(exit code 1)します。

$ echo "fixed the thing" | npx --no-install commitlint
⧗   input: fixed the thing
✖   subject may not be empty [subject-empty]
✖   type may not be empty [type-empty]

✖   found 2 problems, 0 warnings
ⓘ   Get help: https://github.com/conventional-changelog/commitlint/#what-is-commitlint

exit code: 1

規約に沿ったメッセージは何も出力せず、exit code 0 で正常終了します。

$ echo "fix(calcbox): correct rounding error in unit converter" | npx --no-install commitlint
exit code: 0

さらに、commit-msg フックとして実際に組み込んだ状態で git commit を直接実行すると、規約違反のコミットはコミット自体が成立しません(git log に新しいコミットが現れない)。

$ echo "wip stuff" > junk.txt && git add junk.txt
$ git commit -m "update stuff"
⧗   input: update stuff
✖   subject may not be empty [subject-empty]
✖   type may not be empty [type-empty]

✖   found 2 problems, 0 warnings
ⓘ   Get help: https://github.com/conventional-changelog/commitlint/#what-is-commitlint

commit exit code: 1

同じ変更を規約に沿ったメッセージで実行すると、今度は実際にコミットが成立します。

$ git commit -m "chore(scratch): add junk placeholder file"
[main cdcd80a] chore(scratch): add junk placeholder file
 1 file changed, 1 insertion(+)
 create mode 100644 junk.txt
commit exit code: 0

実際にチェンジログを生成する

conventional-changelog-cli を上記の実コミット履歴に対して実行すると、次の CHANGELOG.md が実際に生成されます(BREAKING CHANGE フッターの内容も自動的に専用セクションへ抽出されている点に注目してください)。

$ npx --no-install conventional-changelog -p angular -i CHANGELOG.md -s -r 0
## 1.0.0 (2026-07-19)

- chore(scratch): add junk placeholder file cdcd80a
- feat(devtoolbox)!: drop support for legacy tool ID format 7d77cd8
- fix(calcbox): correct rounding error in unit converter cab1887
- feat: initial project scaffold with commitizen and commitlint config dc68d2a
- feat(blog): add tag-based CTA filter for affiliate cards 6e71014

### BREAKING CHANGE

- tool IDs must now be kebab-case; old camelCase IDs
  registered before v2 will 404. Run the migration script in
  scripts/migrate-tool-ids.js before upgrading.

conventional-changelog-cli の素の出力は type ごとの見出し(Features / Bug Fixes)までは付けてくれず、コミット一覧を並べる程度に留まります。semantic-releaserelease-notes-generator や、後述する release-please はこの上にさらにテンプレートを被せて、### Features / ### Bug Fixes のような見出しでグルーピングした読みやすいリリースノートを生成します。仕組みの本質(コミットメッセージのパース→種別ごとの集計)は同じです。

モノレポでのスコープ活用・CI強制・squashマージの落とし穴

(a) スコープによるパッケージ別チェンジログの絞り込み

このブログのリポジトリ自体が apps/blog / apps/calcbox / apps/devtoolbox / apps/pomodoro / packages/shared を抱えるモノレポです。Conventional Commitsの scope にパッケージ名を書く慣習(feat(calcbox): ... のように)にしておけば、コミット履歴から特定パッケージの変更だけを抽出できます。上の検証リポジトリで実際に試した例です。

$ git log --oneline --grep='(calcbox)'
cab1887 fix(calcbox): correct rounding error in unit converter

$ git log --oneline --grep='(devtoolbox)'
7d77cd8 feat(devtoolbox)!: drop support for legacy tool ID format

$ git log --oneline --grep='(blog)'
6e71014 feat(blog): add tag-based CTA filter for affiliate cards

ただし注意が必要なのは、scope はあくまでコミットメッセージという自己申告のテキストであり、実際に変更されたファイルパスと一致する保証はない点です。厳密にパッケージ単位でチェンジログを分離したい場合、 LernaNxscope の文字列一致ではなく、コミットが実際にどのパッケージのディレクトリに触れたかgit log -- apps/calcbox 相当)を基準にチェンジログを分割します。scope は人間が読むときの手がかり、実際のパッケージ境界の判定はパスベース、という二段構えで運用するのが確実です。

(b) CIでの強制(commitlintのGitHub Actions連携)

ローカルのgitフックはバイパスできてしまう(--no-verify や、フックが設定されていない環境からのpush)ため、CI側でも同じ検証を実行してPRをブロックするのが実務での定石です。commitlint はPR上の全コミットメッセージを検証する公式GitHub Actionを提供しています。

# .github/workflows/commitlint.yml
name: Lint Commit Messages
on:
  pull_request:

jobs:
  commitlint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: wagoid/commitlint-github-action@v6

このジョブは、PRに含まれる各コミット(および --amendrebase 後の最新状態)に対して commitlint を実行し、1つでも規約違反があればジョブを失敗させます。GitHubのブランチ保護ルールで「このステータスチェックが成功しないとマージできない」を必須に設定しておけば、ローカルのフックを迂回されても、マージの手前で機械的に弾くという最終防衛線が機能します。

(c) squashマージとの相互作用という落とし穴

GitHub/GitLabで「Squash and merge」を使っている場合によく見落とされる点があります。PRブランチ内の個々のコミットメッセージは、squashマージ後は履歴に残らず、squashコミット1つのメッセージだけがmainブランチの履歴・チェンジログ・semverバンプ判定に使われるということです。

  • PRブランチの途中コミット(wip, fix typo, address review comments など雑多なメッセージ)が規約に沿っていなくても、squash後には消えるため実害はない
  • ただし、GitHubのデフォルト挙動ではsquashコミットのメッセージにPRタイトルがそのまま使われるため、実質的に「規約に従うべき対象はPRタイトルである」という運用に置き換わる
  • そのため、コミット単位ではなくPRタイトル単位で commitlint を検証するワークフロー(pull_request イベントで github.event.pull_request.title を検証対象にする)にしておかないと、squashマージ前提のプロジェクトでは検証が実質的に無意味になる

上記のGitHub Actionの例は「PR内の各コミット」を検証するモードですが、squashマージ運用のリポジトリでは、wagoid/commitlint-github-action の設定を「PRタイトルのみを検証する」モードに切り替えるか、別途PRタイトルをlintする仕組み(例: amannn/action-semantic-pull-request)を併用する必要があります。この違いを意識しないまま運用すると、「commitlintは通っているはずなのに、mainブランチの履歴もチェンジログもConventional Commitsに従っていない」という食い違いが起こります。

最近の動向:semantic-release・standard-version・release-please

Conventional Commitsを起点にバージョンを自動決定するツールの勢力図は、この数年で変化しています。

  • standard-version は2022年5月にメンテナが公式にメンテナンス終了を表明しました。GitHubを使っている場合は後継として release-please(Googleが開発)への移行が推奨されており、GitHub Actionsが使えない環境向けにはフォーク版の commit-and-tag-version が案内されています。
  • semantic-release は「pushするたびにコミットを解析し即座にリリースする」継続的デリバリー志向のツールとして引き続き広く使われています。プラグイン機構(commit-analyzerrelease-notes-generatorchangelognpm/github/git publish)で本記事の解析ロジックを実装しています。
  • release-please はPR上に「次のリリースで何が変わるか」を常に反映した「Release PR」を維持する方式で、semantic-releaseのようにマージ即リリースするブラックボックスではなく、リリース内容を人間がレビューしてからマージできる点が特徴です。
  • モノレポでは、Conventional Commitsのパース自体に依存しない Changesets も選択肢として広がっています。開発者がPRごとに変更内容を記述した「changesetファイル」を追加し、CIボットがバージョンアップ用PRを自動生成する方式で、scope の自己申告に頼らずパッケージ単位の変更管理ができます。

いずれを選ぶにせよ、土台にあるのは本記事で解説したConventional Commitsの構文解析ロジックであり、Commitizenはその入力を人間が間違えにくくするための最初の一歩、という位置づけです。

Commitizenを導入することで、チーム全体でコミットメッセージの品質を向上させ、プロジェクトの管理をより効率的に行うことができます。さらにcommitlintによるCI強制、モノレポでのスコープ運用、squashマージ時のPRタイトル運用まで含めて設計することで、初めてConventional Commits本来の自動化価値(バージョン自動決定・チェンジログ自動生成)を安全に引き出せます。

関連記事

参考文献