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 形式の構造

Markdown — 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 ルール

コミットメッセージ — 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 ドラフトを生成

Python — 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 からバージョン一覧を抽出

Python — CHANGELOG.md を解析してバージョン一覧を表示
"""
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 スクリプトを組み合わせてドラフト自動生成を仕組み化し、人間が最終確認・肉付けをするワークフローが長続きします。