CHANGELOG とは何か
CHANGELOG(チェンジログ)は、ソフトウェアの各バージョンで何が変更されたかを時系列に記録するドキュメントです。 ユーザー・開発者・運用担当者がバージョンアップ時の影響範囲を把握するために参照します。
CHANGELOG がない状態では、利用者は「このバージョンに何が変わったか知るために git log を全部読む」羽目になります。 また、運用担当者は「先週のデプロイで何が変わったか」を口頭で確認するしかなくなります。
💡 git log は CHANGELOG ではない
git log のコミットメッセージは開発者視点の記録です。CHANGELOG は「利用者・運用者が知りたい変更」を人間が読める形式で整理したものです。両者の粒度は異なります。
バージョン体系(Semantic Versioning)
CHANGELOG のバージョン番号は Semantic Versioning(SemVer) — MAJOR.MINOR.PATCH 形式が業界標準です。
| 番号 | 意味 | いつ上げるか | 例 |
|---|---|---|---|
| MAJOR | 後方互換性を破壊する変更 | API の破壊的変更・大規模リファクタ | 1.0.0 → 2.0.0 |
| MINOR | 後方互換性を保った機能追加 | 新 API エンドポイント追加・新機能 | 1.2.0 → 1.3.0 |
| PATCH | 後方互換性を保ったバグ修正 | バグ修正・軽微な改善 | 1.2.3 → 1.2.4 |
また、CHANGELOG には [Unreleased] セクションを設けて、次のリリースに含まれる変更を随時追記していく運用が推奨されます。
リリース時に [Unreleased] セクションをバージョン番号と日付に書き換えます。
6つの変更カテゴリ
Keep a Changelog では変更を次の6カテゴリに分類します。各エントリをカテゴリ別に整理することで、読み手が必要な情報を素早く見つけられます。
| カテゴリ | 英語表記 | 含む変更の種類 |
|---|---|---|
| 追加 | Added | 新機能・新 API・新設定オプションの追加 |
| 変更 | Changed | 既存機能の動作変更(互換性を保った変更) |
| 廃止予定 | Deprecated | 将来のバージョンで削除予定の機能・API |
| 削除 | Removed | 廃止された機能・API の削除 |
| 修正 | Fixed | バグ修正・不具合対応 |
| セキュリティ | Security | 脆弱性対応・セキュリティ改善 |
⚠️ 空のカテゴリは省略する
該当する変更がないカテゴリは記載不要です。全カテゴリを毎回書くと CHANGELOG が冗長になります。
Keep a Changelog 形式の構造
# CHANGELOG
このプロジェクトのすべての変更を記録します。
形式は [Keep a Changelog](https://keepachangelog.com/ja/1.0.0/) に従い、
バージョン体系は [Semantic Versioning](https://semver.org/lang/ja/) に従います。
## [Unreleased]
### Added
- 注文キャンセル API(POST /orders/{id}/cancel)を追加
---
## [2.3.0] — 2026-06-10
### Added
- CSV エクスポート機能(GET /orders/export)を追加
- 注文検索に日付範囲フィルターを追加(`from` / `to` クエリパラメータ)
### Changed
- 注文一覧 API のデフォルトソート順を「作成日降順」に変更
### Fixed
- 同一 IP からの連続リクエスト時にレート制限が誤作動する問題を修正 (#241)
---
## [2.2.1] — 2026-05-28
### Security
- `orders` テーブルへの SQL インジェクション脆弱性を修正(CVE-2026-XXXX)
### Fixed
- 注文金額が 1,000,000 円を超える場合に 500 エラーが発生する問題を修正 (#229)
---
## [2.2.0] — 2026-05-15
### Added
- 請求書 PDF ダウンロード機能を追加
### Deprecated
- `/v1/orders` エンドポイント(2027-05 に削除予定。`/v2/orders` を使用してください)
### Removed
- `/legacy/orders` エンドポイントを削除(2.0.0 で Deprecated 済み)
---
[Unreleased]: https://github.com/example/my-app/compare/v2.3.0...HEAD
[2.3.0]: https://github.com/example/my-app/compare/v2.2.1...v2.3.0
[2.2.1]: https://github.com/example/my-app/compare/v2.2.0...v2.2.1
[2.2.0]: https://github.com/example/my-app/releases/tag/v2.2.0
記述ルール
| ルール | OK 例 | NG 例 |
|---|---|---|
| 利用者目線で書く | 「注文一覧 API の応答速度を 50% 改善」 | 「N+1 クエリを解消してキャッシュを追加」 |
| 過去形で書く | 「エクスポート機能を追加した」→「エクスポート機能を追加」 | 「エクスポート機能を追加します」 |
| Issue/PR 番号を付ける | バグ修正 (#241) | バグ修正 |
| Breaking Change を明示 | ⚠️ **BREAKING:** `orderId` フィールドを `order_id` に変更 | フィールド名を変更(Breaking と書かない) |
| Deprecated に削除予定日を書く | 2027-05 に削除予定 | 近いうちに削除予定 |
Python で git log から CHANGELOG を自動生成する
Conventional Commits(feat: / fix: / docs: などのプレフィックス)に従ったコミットメッセージが蓄積されている場合、Python で git log を解析して CHANGELOG ドラフトを自動生成できます。
① Conventional Commits ルール
# 形式: ():
# type: feat / fix / docs / style / refactor / test / chore / perf / security
feat(orders): CSV エクスポート機能を追加
fix(orders): レート制限が誤作動する問題を修正 (#241)
security(db): SQL インジェクション脆弱性を修正 (#242)
feat(invoice)!: 請求書 PDF ダウンロードを追加 # ! = BREAKING CHANGE
② git log から CHANGELOG ドラフトを生成
"""
Conventional Commits 形式の git log を解析して
CHANGELOG.md の [Unreleased] セクションのドラフトを生成する。
使い方:
python gen_changelog.py # 最新タグから HEAD まで
python gen_changelog.py v2.2.0 v2.3.0 # タグ間を指定
"""
import re
import subprocess
import sys
from collections import defaultdict
from datetime import date
TYPE_TO_SECTION = {
"feat": "Added",
"fix": "Fixed",
"security": "Security",
"perf": "Changed",
"refactor": "Changed",
"docs": "Changed",
"style": "Changed",
"chore": "", # CHANGELOG に含めない
"test": "",
}
COMMIT_PATTERN = re.compile(
r"^(?P\w+)(?:\((?P[^)]+)\))?(?P!)?: (?P.+)$"
)
def get_commits(from_ref: str = "", to_ref: str = "HEAD") -> list[str]:
"""git log から コミットメッセージ一覧を返す。"""
range_arg = f"{from_ref}..{to_ref}" if from_ref else to_ref
out = subprocess.check_output(
["git", "log", "--pretty=format:%s", range_arg],
text=True
)
return [line.strip() for line in out.splitlines() if line.strip()]
def get_latest_tag() -> str:
"""最新タグを返す(なければ空文字)。"""
try:
return subprocess.check_output(
["git", "describe", "--tags", "--abbrev=0"],
text=True, stderr=subprocess.DEVNULL
).strip()
except subprocess.CalledProcessError:
return ""
def parse_commits(messages: list[str]) -> dict[str, list[str]]:
sections = defaultdict(list)
for msg in messages:
m = COMMIT_PATTERN.match(msg)
if not m:
continue
type_ = m.group("type").lower()
scope = m.group("scope") or ""
subject = m.group("subject")
breaking = bool(m.group("breaking"))
section = TYPE_TO_SECTION.get(type_, "")
if not section:
continue
prefix = f"**[BREAKING]** " if breaking else ""
scope_str = f"({scope}) " if scope else ""
sections[section].append(f"- {prefix}{scope_str}{subject}")
return sections
def render_changelog(sections: dict[str, list[str]]) -> str:
lines = [f"## [Unreleased] — {date.today().isoformat()}", ""]
order = ["Added", "Changed", "Deprecated", "Removed", "Fixed", "Security"]
for section in order:
if section in sections:
lines.append(f"### {section}")
lines.extend(sections[section])
lines.append("")
return "\n".join(lines)
if __name__ == "__main__":
if len(sys.argv) == 3:
from_ref, to_ref = sys.argv[1], sys.argv[2]
elif len(sys.argv) == 2:
from_ref, to_ref = sys.argv[1], "HEAD"
else:
from_ref = get_latest_tag()
to_ref = "HEAD"
print(f"# 範囲: {from_ref or '(全件)'}..{to_ref}\n")
messages = get_commits(from_ref, to_ref)
sections = parse_commits(messages)
print(render_changelog(sections))
③ 既存 CHANGELOG からバージョン一覧を抽出
"""
CHANGELOG.md を解析してバージョン・日付・変更数を一覧表示する。
"""
import re
from pathlib import Path
CHANGELOG = Path("CHANGELOG.md")
VERSION_HEADER = re.compile(r"^##\s+\[([^\]]+)\](?:\s+[—–-]\s+(\d{4}-\d{2}-\d{2}))?", re.MULTILINE)
ENTRY = re.compile(r"^-\s+", re.MULTILINE)
def parse_changelog(text: str) -> list[dict]:
versions = []
matches = list(VERSION_HEADER.finditer(text))
for i, m in enumerate(matches):
version = m.group(1)
release_date = m.group(2) or "未リリース"
start = m.end()
end = matches[i + 1].start() if i + 1 < len(matches) else len(text)
section_text = text[start:end]
count = len(ENTRY.findall(section_text))
versions.append({"version": version, "date": release_date, "entries": count})
return versions
if __name__ == "__main__":
if not CHANGELOG.exists():
print("CHANGELOG.md が見つかりません")
else:
text = CHANGELOG.read_text(encoding="utf-8")
versions = parse_changelog(text)
print(f"{'バージョン':<15} {'日付':<12} {'変更数':>5}")
print("-" * 36)
for v in versions:
print(f"{v['version']:<15} {v['date']:<12} {v['entries']:>5} 件")
CHANGELOG のアンチパターン
| アンチパターン | 問題 | 対策 |
|---|---|---|
| git log そのまま | 開発者視点で利用者には意味不明 | 利用者目線で要約して記述する |
| バージョンなしの日付のみ | どのバージョンにいつ何が含まれるか追跡できない | SemVer バージョン番号を必ず付ける |
| Breaking Change が目立たない | 移行対応が遅れてトラブルになる | ⚠️ や [BREAKING] プレフィックスで強調する |
| Deprecated に期限がない | いつまでも削除されない | 「2027-XX に削除予定」と具体的日付を書く |
| リリース後に追記しない | 次の担当者がリリース内容を把握できない | PR マージ前に [Unreleased] への追記を義務化する |
まとめ
✅ CHANGELOG で定義すべき要素
□ バージョン番号(SemVer: MAJOR.MINOR.PATCH)
□ リリース日(YYYY-MM-DD 形式)
□ [Unreleased] セクション(次リリース分を随時追記)
□ 6カテゴリ(Added / Changed / Deprecated / Removed / Fixed / Security)
□ 利用者目線の変更説明(Issue / PR 番号付き)
□ Breaking Change の明示(⚠️ で強調)
□ Deprecated の削除予定日
CHANGELOG は「リリースのたびに更新するもの」という習慣が定着するかどうかが成否を分けます。 Conventional Commits と Python スクリプトを組み合わせてドラフト自動生成を仕組み化し、人間が最終確認・肉付けをするワークフローが長続きします。