🎄

コミットメッセージからリリースを自動化する技術

に公開

Introduction

こんにちは、chckと申します。普段はAI Labという研究部門でResearch Engineerとして他チームの実験サポートや研究成果の社会実装をしています。

皆さんはアプリケーション(アプリ)のリリースバージョンを意識したことはありますか?ここでいうバージョンとは、機能追加やバグ修正による差分を区別するために付けられる1.0.0のような識別番号のことです。バージョン管理されている場合、アップデートによってどのような変更が行われたのかをユーザーは簡単に把握できます。また、不具合があった際のバグの再現や前バージョンの復元といった対応が容易です。プロダクションコードに限らず、個人プロジェクトや開発環境でもバージョン管理をするメリットは大いにあります。私はWeb系企業の研究所のエンジニアとして、研究コードをプロダクトへ繋げる仕事をしています。そういった経験上、当初は自分だけの書き捨てコードを想定していても、最終的には誰かへの共有が目的になることが多いです。プロジェクトを後輩に引き継ぐことになったり、デモとしてユーザーに見せることになったり、誰にも共有する予定はなくても過去の自分が書いたコードを未来の自分(≒他人)が時間を置いて見返すことなど、様々なシチュエーションで役立つのが今回紹介するバージョン管理の自動化というわけです。

まずは、アプリのリリースにおけるバージョン管理を取り巻くエコシステムについて説明します。

SemVer (Semantic Versioning)とは

SemVerは、ソフトウェアのバージョンを管理するための規約です。

<valid semver> ::= <version core>
                 | <version core> - <pre-release>
                 | <version core> + <build>
                 | <version core> - <pre-release> + <build>

<version core> ::= <major> . <minor> . <patch>

具体的にはアプリのアップデートで使用される1.0.0-beta -> 1.0.0 -> 1.1.0のようなバージョン番号の形式です。

Type Description Example
major 破壊的変更を伴う大規模な変更。 1.0.0 -> 2.0.0
minor 後方互換性を持つ機能の追加。 1.0.0 -> 1.1.0
patch 後方互換性を持つ微修正。 1.0.0 -> 1.0.1

例えば有名なOSSやゲームのメジャーアップデートの発表は大きな変更が期待されるため、ユーザーコミュニティが盛り上がるのも頷けるでしょう。

Conventional Commitsとは

先程紹介したSemVerに準拠するコミットメッセージの規約です。

<type>[optional scope]: <description>
  |     |                 |
  |     |                 └ コミット内容の説明
  |     |
  |     └ コミットの影響範囲を指定(省略可)
  |
  └ コミットの種類(!で破壊的変更を示す)

これに則り、コミットメッセージは以下の形式で書きます。

feat: add text preprocessor for pdf

カッコ付けで変更対象を追記することもできます。

fix(pdf_cleaner): organize db connection to create engine

破壊的変更を示すには<type>の後ろに!を付けます。

chore!: drop support for Node 6 to use JavaScript features not available in Node 6.

またはBREAKING CHANGEフッターを付けてもOKです。

chore: drop support for Node 6
BREAKING CHANGE: use JavaScript features not available in Node 6.

typeは全部で10種類近くありますが、後述するツールによってはtypeの選択肢を減らしてシンプルにする等のカスタマイズが可能です。

type description
fix バグ修正。SemVerのpatchに対応。
feat 新機能。SemVerのminorに対応。
<type>! 任意のtypeと組み合わせて破壊的変更を表す。SemVerのmajorに対応。
docs ドキュメントの変更。
style フォーマットなどコードの動作に影響しない見た目の変更。
refactor バグ修正や機能追加を含まないリファクタリング。
perf パフォーマンスを向上させるコードの変更。
test テストの追加、または既存テストの修正。
build ビルドシステムや外部依存関係の変更。
ci CIの設定ファイルやスクリプトの変更。
chore 上記どれにも当てはまらないビルドプロセスや補助ツールの変更。

Approach

前置きが長くなりましたがここからはタイトルの通り、コミットメッセージからリリースを自動化する仕組みを作っていきます。Python環境を前提としていますが、他の言語でも依存ライブラリの他、全体の流れは変わりません。

git version 2.52.0
Python 3.14.2
uv 0.9.16 (a63e5b62e 2025-12-06)
commitizen 4.9.1
pre-commit 4.5.0

コミットメッセージの標準化とバージョン管理

まずは、git repositoryを作成します。完成版のコードもGitHubに上げているのでそちらを見ながら追いかける形でもOKです。

mkdir python-semver && cd $_
git init

ディレクトリが作成されたら、uv コマンドを使用してPythonプロジェクトを初期化します。 --lib はライブラリとしてプロジェクトを初期化するオプションです。

uv init --name psv --lib

projectのversionを確認しておきます。これはpyproject.tomlのproject.versionを参照しています。

uv version

0.1.0

現段階ではエントリーポイントがないので、メイン関数src/psv/__main__.pyを用意します。

# src/psv/__main__.py
from psv import hello

def main() -> None:
    print(hello())

if __name__ == "__main__":
    main()

uv run を用いて現段階で実行すると「Hello from psv!」の文字列が出力されます。

uv run -m psv

Hello from psv!

ここで、commitizenというライブラリをインストールします。

uv tool install commitizen

commitizenは、projectのSemantic Versioningの管理やコミットメッセージがConventional Commitsに準拠するような機能が入ったオールインワンのツールです。Semantic Versioningをサポートするツールはたくさんありますが、Python Projectで小さく始めるなら今回紹介するcommitizenを、PyPIへの自動アップロードも含めるならpython-semantic-releaseをおすすめします。

ツール エコシステム 特徴
Commitizen Node.js / Python インタラクティブなCUIを通じて、開発者がどんな変更答えていくだけで正しい形式のコミットを作成できる。
Commitlint Node.js コミットメッセージが規約を守っているかを検証し、違反があればコミットをブロックする。
semantic-release Node.js コミットログ(featfix)から次のバージョン番号の決定、タグ付け、Changelog生成、npm/GitHubへの公開を全自動で行う。
python-semantic-release Python semantic-release のPython版。PyPIへのアップロードや pyproject.toml のバージョン書き換えなど、Pythonに特化。

cz commandが使えるようになったら初期設定を行います。

cz init

対話形式でオプションが聞かれるので、version_providerをuv、tag_formatをv$version、version_schemeをsemver2、pre-commit hookはcommit-msgpre-commitを有効化し、それ以外はデフォルトでOKです。

cz_init

pyproject.tomlに追記されたcommitizenの設定が確認できます。

# pyproject.toml
 [build-system]
 requires = ["uv_build>=0.9.16,<0.10.0"]
 build-backend = "uv_build"
+
+[tool.commitizen]
+name = "cz_conventional_commits"
+tag_format = "v$version"
+version_scheme = "semver2"
+version_provider = "uv"
+update_changelog_on_bump = true
+major_version_zero = true

しれっと追加された.pre-commit-config.yamlは、pre-commitの設定ファイルです。
pre-commitは、git commandに反応してコードの整形やチェックなど任意のコマンドを実行するツールです[1]。追加したコードがlintを通っているかPull RequestなどCI上で確認する設定はよくありますが、pre-commitのメリットはローカルのgit push前にCI相当のチェックができることです。

追加された.pre-commit-config.yamlは古い依存の可能性があるので、以下のコマンドで更新しましょう。pre-commit コマンドが入っていない場合はこの段階で公式からインストールしておいてください。

pre-commit autoupdate

pre-commitの設定ファイルは以下のようになっています。詳細は割愛しますが、git commit前 (stage: commit-msg) とgit push前 (stage: pre-push) にcommitizenが実行されるんだなーくらいの認識で大丈夫です。

# .pre-commit-config.yaml
repos:
- hooks:
  - id: commitizen
  - id: commitizen-branch
    stages:
    - pre-push
  repo: https://github.com/commitizen-tools/commitizen
  rev: v4.10.0

ここまでの内容をcommitしておきましょう。

git add .
cz commit
git push

cz commit はgit commitの代わりとなるcommitizenのコマンドです。conventional commitsに従ったprefixに矯正してくれます。慣れてきたらcz-git[2]やclaude[3]などのLLMで生成させることもできます。

cz_commit

次に、バージョンを上げるコマンドcz bumpを試してみましょう。--changelogは変更差分をCHANGELOG.mdとして生成し、--check-consistencyはバージョンの整合性をチェックするオプションです。

cz bump --changelog --check-consistency

git tagを作成するか聞かれるのでYesで進めます。

cz_dump

git commitまで行われるのでどんな変更が起こったか確認してみます。

git diff HEAD^

CHANGELOG.mdの作成とpyproject.toml及びuv.lockのバージョン更新が行われています。

git_diff

Dynamic Versioningによるバージョンの一元管理

基本的な動きの説明はこれで終わりですが、ここでもう一工夫します。
本来ならばバージョン番号の管理は一つで十分ですが、現状だとgit tag、pyproject.toml、uv.lockの3つで行われています。pyproject.tomlやuv.lockのバージョン番号は変数化し(dynamic-versioningと呼びます)、このprojectのバージョン番号は全てgit tag (VCS tag) を参照するように変更します。

uv-dynamic-versioningというツールを使うため、pyproject.tomlを以下のように変更します。

# pyproject.toml
 [project]
 name = "psv"
-version = "0.2.0"
+dynamic = ["version"]
 description = "Add your description here"
 readme = "README.md"
 authors = [
@@ -10,13 +10,30 @@ requires-python = ">=3.12"
 dependencies = []

 [build-system]
-requires = ["uv_build>=0.9.16,<0.10.0"]
-build-backend = "uv_build"
+requires = ["hatchling", "uv-dynamic-versioning"]
+build-backend = "hatchling.build"

 [tool.commitizen]
 name = "cz_conventional_commits"
 tag_format = "v$version"
 version_scheme = "semver2"
-version_provider = "uv"
+version_provider = "scm"
+version_files = [
+    "src/psv/__version__.py:version",
+]
 update_changelog_on_bump = true
 major_version_zero = true
+
+[tool.uv-dynamic-versioning]
+vcs = "git"
+style = "semver"
+format = "{base}"
+
+[tool.hatch.version]
+source = "uv-dynamic-versioning"
+
+[tool.hatch.build.hooks.version]
+path = "src/psv/__version__.py"
+template = '''
+version = "{version}"
+'''

ここでは以下の変更を行っています。

  • build backendをHatchへ切り替え: 最近uvのdefault build backendになったuv_buildはdynamic-versioningに必要な機能をサポートしていない[4]ため、従来使われていたHatchへ切り替える。
  • commitizenの設定: versionの取得方法をuv (pyproject.toml) からgit (SCM) に変更。
  • uv-dynamic-versioningの導入: git tagを参照してバージョン番号を動的に取得する設定を追加。
  • __version.py__の書き出し: package buildのタイミングでソースコードにバージョン番号を埋め込む。

この変更によって作成される__version__.pyをgitで管理してしまうとdynamic-versioningを入れる前と変わらず本末転倒なので、.gitignoreで管理外にしておきます。

# .gitignore
+
+# Ignore static version file to use dynamic versioning
+__version__.py

更に、__main__.pyでversionを出力するようにします。

# src/psv/__main__.py
 from psv import hello
+from .__version__ import version

 def main() -> None:
-    print(hello())
+    print(hello(), f"Version is {version}")

 if __name__ == "__main__":
     main()
uv run -m psv

Hello from psv! Version is 0.2.0

ここまでの内容をcommitし、バージョンも上げてみましょう。helloと一緒にバージョン番号を出力するように変更したので、commit messageは feat: print version number with hello としておきます。

git add .
cz commit
git push

参考までに、現在のコミット履歴は以下のようになります。コミット粒度は差があるとして、前回の0.2.0からfeat typeの差分があればOKです。featはminor versionに対応するので、次のバージョンは0.2.0から0.3.0に上がることが予想できます。

* 1086ce8 (HEAD -> main) feat: print version number with hello
* 7984b6f chore: manage version by git tag as dynamic-versioning
* 4d49c25 (tag: v0.2.0) bump: version 0.1.0 → 0.2.0
* b0c9f0f build: add commitizen and pre-commit to support semantic versioning
* cfe404e feat: add entrypoint as uv project
* 323a251 chore: add gitignore via `gibo dump python > .gitignore`
* 979f2cb (origin/main, origin/HEAD) Initial commit

それでは、バージョンを上げてみましょう。

cz bump --changelog --check-consistency

cz_bump_v3

前回の0.2.0と異なり、dynamic-versioningを導入したのでpyproject.tomlとuv.lockへのバージョン書き出しがなくなりスッキリしました。

git_diff_v3

uv run -m psv

Hello from psv! Version is 0.3.0

また、ここまでのgit tag (v0.2.0, v0.3.0) がまだremoteに反映されていないので、合わせてpushしておきます。

git push origin --tags
Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/chck/python-semver.git
 * [new tag]         v0.2.0 -> v0.2.0
 * [new tag]         v0.3.0 -> v0.3.0

CIによるバージョンアップの自動化

実際のプロジェクトの運用を想定した場合、バージョンアップを毎回手動で行うのは面倒なのでCIで自動化します。このCIが動くタイミングはプロジェクトのリリースルールに依存しますが、基本的には以下のようなケースになるでしょう。

  • a. main (release) branch push (merge) 時に自動でバージョンを上げる
  • b. 自動でバージョンは上げず、任意のタイミングで現在のbranchの最新のコミットを基にバージョンを上げる

今回はbの方法を試します。とはいってもGitHub Actionsのtriggerくらいの差しかないのでどちらも簡単に導入できます。

まず.github/workflows/release.yml にGitHub Actionsのワークフローを定義します。

# .github/workflows/release.yml
name: Release new version

on:
  workflow_dispatch:  # Allow manual execution

jobs:
  release-new-version:
    runs-on: ubuntu-latest
    name: Release new version
    permissions:
      contents: write
    steps:
      - name: Check out
        uses: actions/checkout@v6
        with:
          fetch-depth: 0
      - id: cz
        name: Create bump and changelog
        uses: commitizen-tools/commitizen-action@master
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          changelog_increment_filename: body.md
      - name: Print Version
        run: echo "Bumped to version v${{ env.REVISION }}"
      - name: Create release note from changelog
        uses: softprops/action-gh-release@v2
        with:
          body_path: body.md
          tag_name: v${{ env.REVISION }}

Github Actions用のCommitizenを使ってバージョンを上げたあと、changelogをGitHubのRelease Noteとして出力しています。secrets.GITHUB_TOKENはgithub actions実行時に設定されるデフォルトのトークンで筆者の環境では問題なく動きましたが、皆さんの環境で権限が足りない場合は別途発行したものに置き換えてください。env.REVISIONにはバージョンアップしたgit tagが入るので、それをtag_nameとして参照しています。

commit messageは ci: add ci to manage version and release note としてgit commitします。

git add .
cz commit

更に、Release Noteをわかりやすくするためfeat typeの差分を作ります。

# src/psv/__init__.py
 from .__version__ import version

 def main() -> None:
-    print(hello(), f"Version is {version}")
+    print(hello(), f"Version is v{version}")

 if __name__ == "__main__":
     main()

commit messageは feat: add v prefix to print version number としてここまでの内容をgit pushします。

git add .
cz commit
git push

GitHubのUIから作ったWorkflowの動作確認をしてみましょう。筆者は https://github.com/chck/python-semver というRepositoryを作っていますが、ここは皆さんの環境に適宜変更してください。

該当Repositoryのトップページ上メニューのActions -> 左のRelease new version -> Run workflowから実行します。

ci_release

Workflowが終了するのを待ちます。数分後CIによって0.3.0から0.4.0にバージョンアップされており、リリースノートも正しく作成されていることが確認できました。

release_note_v4

駆け足での紹介となりましたが、実際のデモは以上になります。ここまでの内容はGitHubで公開しているので参考にしてください。

Summary

コミットメッセージの規約であるConventional Commitsをベースとしたアプリのリリース管理について、Python Projectを例に紹介しました。今回は簡単のためpre-commitで走るLinterはコミットメッセージのチェックのみとしましたが、もちろんrufftyといったPythonのLinterも組み込むことができます。またCIでも同じ設定のpre-commitを走らせることで、ローカルでのチェックに加えgit push後もコードの品質の維持が可能です。バージョン管理やリリースノートの生成にはそのRepositoryの全commit messageを元にしているため、開発者全員の意識改革が必要です。普段コミットメッセージをなんとなく書いていた方は今回話した内容を意識すると、将来的にアプリケーションの品質が向上するだけでなく未来の自分を含むソースコードを共有するユーザーの助けになるはずです。一度導入してしまえばあとは仕組みが自動でサポートしてくれるので、個人プロジェクトからでも早速試してみてください。

明日9日目のCyberAgent AI Lab Advent Calendar 2025は、原口さんによる「ン」と「ソ」の境界はどこ?です。お楽しみに。

References

脚注
  1. Frontendではhuskyが同様のツールとして有名ですね ↩︎

  2. https://cz-git.qbb.sh/recipes/openai ↩︎

  3. https://spiess.dev/blog/how-i-use-claude-code#claude-sdk ↩︎

  4. https://github.com/astral-sh/uv/issues/14561 ↩︎

Discussion