#11 pyproject.tomlとhatch-vcs: gitタグから自動バージョニング - Python OSS開発記録
python 15 min read

#11 pyproject.tomlとhatch-vcs: gitタグから自動バージョニング - Python OSS開発記録

avatar-m-1

karrinn

著者

リリースのたびに「pyproject.tomlのversionを書き換えて...」ってめちゃくちゃ面倒じゃないですか?
hatch-vcsを使えば、gitタグからバージョン番号を自動取得できます。今回はwagtail-reusable-blocksでの設定を振り返ります。

KEY TAKEAWAYS

この記事でわかること

  • 手動バージョン管理の問題点と自動化のメリット
  • hatch-vcsの仕組み(gitタグ → バージョン番号)
  • pyproject.tomlのdynamic versioningの設定

用語の定義

TERM 01

hatch-vcs

Hatchling用のプラグイン。VCS (Version Control System) のタグからバージョン番号を自動取得。gitタグ「v0.1.0」からバージョン「0.1.0」を生成します。

TERM 02

pyproject.toml

Pythonプロジェクトのメタデータファイル (PEP 518/621)。パッケージ名・バージョン・依存関係・ビルド設定などを一元管理します。setup.pyの後継です。

TERM 03

uv

Rust製の超高速Pythonパッケージマネージャー。pip/pip-toolsの代替で、キャッシュ利用時は80〜115倍速いとされています。Astral社が開発しています。

TERM 04

Dynamic Versioning

バージョン番号をビルド時に動的に決定する仕組み。pyproject.tomlで「dynamic = ["version"]」と指定し、VCSタグから自動取得します。

参考: PEP 621 / hatch-vcs GitHub

手動バージョン管理の問題

従来は、リリースのたびにpyproject.tomlのversionを手動で書き換える必要がありました。これ、経験したことありませんか?

よくある手動バージョン管理の悲劇

従来の手動フロー

  1. pyproject.tomlを開く
  2. version = "0.0.9"version = "0.1.0" に書き換え
  3. コミット・プッシュ
  4. タグ作成 git tag v0.1.0
  5. タグプッシュ git push origin v0.1.0
  6. リリース → あれ、pyproject.tomlとタグのバージョンが違う!

よくある問題

問題1: バージョン不一致

pyproject.tomlは「0.1.0」、gitタグは「v0.0.10」← どっちが正しい?
→ ユーザー混乱、パッケージ情報が信頼できない

問題2: 書き換え忘れ

タグは「v0.1.0」なのに、pyproject.tomlは「0.0.9」のまま
→ PyPIに「0.0.9」として公開される、タグと矛盾

問題3: コミット汚染

バージョン書き換えだけのコミットが量産される
→ 「chore: bump version to 0.1.0」← これ本当に必要?

問題4: 手順が多い

ファイル編集 → コミット → タグ作成 → プッシュ
ヒューマンエラーのリスク(トピック#9参照)

hatch-vcsを使えば全て解決

gitタグがバージョンの唯一の情報源 (Single Source of Truth) になります。 pyproject.tomlのバージョンは自動取得されるので、書き換え不要。 タグをpushするだけで、正しいバージョンが自動設定されます。

参考: PEP 621 – Storing project metadata in pyproject.toml

hatch-vcsの仕組み

hatch-vcsはビルド時にgitタグを読み取り、バージョン番号を自動生成します。内部ではsetuptools-scm(v8.2.0以上)を使用しています。

動作の流れ

hatch-vcs-explanation

バージョン番号の変換ルール

gitタグ生成されるバージョン説明
v0.1.00.1.0先頭の「v」を削除
v0.1.0-rc10.1.0rc1リリース候補版
タグなし0.0.0.dev0+g1234567開発版(コミットハッシュ付き)
タグから3コミット先0.1.1.dev3+g7890abcタグ後の開発版

Single Source of Truth: gitタグがバージョンの唯一の情報源。 pyproject.toml、__version__、パッケージメタデータ、全て同じバージョンが自動設定される。 不一致が起きない仕組みです。

参考: hatch-vcs - PyPI / Versioning - Hatch

実装: pyproject.toml設定

wagtail-reusable-blocksの実際の設定を見ていきましょう。PEP 621に準拠したモダンな構成です。

[project] - 基本情報

ポイント

PEP 621で標準化されたメタデータ形式です。dynamic = ["version"]を指定することで、バージョンを動的に取得することを宣言します。

  • dynamic: 動的に決定するフィールドを宣言
  • version = "x.x.x" の記述は不要
  • nameだけは動的にできない(PEP 621の制約)
pyproject.toml
[project]
name = "wagtail-reusable-blocks"
dynamic = ["version"]
description = "Reusable content blocks..."
readme = "README.md"
license = { text = "BSD-3-Clause" }
authors = [{ name = "kkm-horikawa" }]
requires-python = ">=3.10"
dependencies = [
"django>=4.2",
"wagtail>=5.0",
]

[build-system] - ビルド設定

ポイント

ビルドバックエンドとしてHatchlingを使用し、hatch-vcsプラグインを追加します。PEP 517に準拠した設定です。

  • requires: ビルドに必要なパッケージ
  • hatchling: モダンなビルドバックエンド
  • hatch-vcs: VCSバージョニングプラグイン
pyproject.toml
[build-system]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"

[tool.hatch.version] - バージョン取得設定

ポイント

バージョンをVCS(Git)から取得することを指定します。version_schemeで開発版番号の生成ルールを制御できます。

  • source = "vcs": Gitからバージョン取得
  • no-guess-dev: 次バージョンを推測しない
  • CI/CD環境ではfetch-depth: 0が必要
pyproject.toml
[tool.hatch.version]
source = "vcs"

[tool.hatch.version.raw-options]
version_scheme = "no-guess-dev"

[tool.hatch.build.targets.wheel] - パッケージング設定

ポイント

wheelファイル生成時に含めるディレクトリを指定します。wagtail-reusable-blocksではsrc/レイアウトを採用しています。

  • packages: wheelに含めるパッケージパス
  • src/レイアウト: テスト時のimportミスを防止
pyproject.toml
[tool.hatch.build.targets.wheel]
packages = ["src/wagtail_reusable_blocks"]

参考: Writing your pyproject.toml - Python Packaging User Guide

uvでのビルド

wagtail-reusable-blocksでは、uv(超高速Pythonパッケージマネージャー)を使用しています。Astral社(Ruffの開発元)製で、pipより8〜10倍、キャッシュ利用時は80〜115倍速いとされています。

なぜuvを使うのか

圧倒的な速度

Rust製で爆速。
キャッシュなしでも8〜10倍速い。
キャッシュありで80〜115倍速い。

シンプルなコマンド

uv buildだけ。
pip + build よりシンプル。
学習コスト低い。

オールインワン

pip, pip-tools, pipx,
poetry, pyenv, virtualenv
を1ツールで代替可能。

GitHub Actionsでのビルド

トピック#10で紹介したpublish.ymlでの設定例です。uvをインストールし、uv buildを実行するだけでパッケージが生成されます。

  • uvをインストール(astral-sh/setup-uv@v4)
  • uv buildで.whlと.tar.gz生成
  • hatch-vcsが自動でバージョン埋め込み
publish.yml
# GitHub Actionsで実行
- name: Install uv
uses: astral-sh/setup-uv@v4

- name: Build package
run: uv build

# dist/に生成される
# wagtail_reusable_blocks-0.1.0-py3-none-any.whl
# wagtail_reusable_blocks-0.1.0.tar.gz

参考: uv - Astral Docs / uv: Python packaging in Rust - Astral

実際の動作フロー

タグpushからパッケージ生成までの流れを見ていきましょう。開発者がやることはタグをpushするだけです。

完全自動バージョニング

  • 01

    開発者: タグをpush

    git tag v0.1.0 && git push origin v0.1.0
    pyproject.tomlの編集は不要

  • 02

    GitHub Actions: ビルド開始

    publish.yml(トピック#10)がトリガーされます。

  • 03

    checkout: git履歴を全取得

    fetch-depth: 0 でタグ情報も取得します。

  • 04

    uv build: パッケージビルド

    hatchlingがhatch-vcsを呼び出し、gitタグ「v0.1.0」を読み取り、
    バージョン「0.1.0」を生成してパッケージに埋め込みます。

  • 05

    dist/にファイル生成

    wagtail_reusable_blocks-0.1.0-py3-none-any.whl
    wagtail_reusable_blocks-0.1.0.tar.gz

  • 06

    PyPI公開: バージョン0.1.0として公開

    gitタグ、パッケージ、PyPI、全て同じバージョン。不一致なし。

開発者がやること

git tag v0.1.0 && git push origin v0.1.0 だけ。 pyproject.tomlの編集、バージョン番号のコピペ、コミット、全て不要。 タグが全ての情報源です。

参考: wagtail-reusable-blocks - GitHub

OSS vs 企業プロジェクト: バージョン管理戦略

OSSでも企業プロジェクトでも、gitタグをバージョンの唯一の情報源にする考え方は共通です。

OSS: セマンティックバージョニング + hatch-vcs

wagtail-reusable-blocksでは、セマンティックバージョニング(major.minor.patch)を採用しています。

バージョン番号のルール

変更の種類上げる部分
破壊的変更major0.1.01.0.0
新機能追加minor0.1.00.2.0
バグ修正patch0.1.00.1.1

hatch-vcsとの連携

リリースフロー

  1. Release Drafter(トピック#9)がラベルからバージョン推定
  2. 開発者がRelease Draftを確認してタグ名決定(例: v0.2.0)
  3. タグpush → GitHub Actions実行
  4. hatch-vcsがタグからバージョン取得(0.2.0)
  5. PyPIに正しいバージョンで公開

トピック#9・#10・#11の連携: Release Drafter(#9)でバージョン推定 → Trusted Publishing(#10)で自動公開 → hatch-vcs(#11)で正しいバージョン埋め込み。 全て自動化、人間の手作業ゼロです。

企業: Dockerイメージタグ + アプリケーションバージョン

企業プロジェクトでも、gitタグをバージョンの唯一の情報源にする考え方は同じです。

Dockerイメージタグの例

CI/CDでのイメージタグ付け

  • gitタグ v0.1.0 を push
  • CI/CDがタグを読み取り
  • Dockerイメージを myapp:0.1.0 としてビルド
  • コンテナレジストリにpush
  • k8sにデプロイ時に image: myapp:0.1.0 を指定

アプリ内のバージョン表示

Pythonアプリの場合、importlib.metadataを使ってパッケージバージョンを取得できます。ログや管理画面に表示すると、デプロイ時のトラブルシューティングが楽になります。

  • hatch-vcsで __version__ 自動生成
  • ログ・管理画面・APIにバージョン表示
  • gitタグと一致したバージョンが表示される
myapp/__init__.py
from importlib.metadata import version
__version__ = version("myapp")

# ログ出力
import logging
logger.info(f"App version: {__version__}")

# 管理画面フッター
# Version: 0.1.0 (matches git tag v0.1.0)

企業プロジェクトでもgitタグが情報源

PyPIパッケージでもDockerイメージでも、gitタグをバージョンの唯一の情報源にする考え方は同じ。 手動でバージョンファイルを書き換える運用は、ヒューマンエラーの温床(トピック#9参照)です。

参考: セマンティック バージョニング 2.0.0

よくある質問

A: 開発版バージョンが自動生成されます。

hatch-vcsはタグがなくても動作します。 例: 0.0.0.dev0+g1234567(コミットハッシュ付き) 最後のタグから3コミット進んでいたら 0.1.1.dev3+g7890abc のように生成されます。

A: タグ情報を取得するためです。

GitHub Actionsのactions/checkout@v4はデフォルトで最新1コミットのみ取得します。 hatch-vcsはgitタグを読む必要があるため、fetch-depth: 0で全履歴を取得します。 これがないとタグが見えず、バージョン番号を正しく生成できません。

A: 機能はほぼ同じですが、ビルドバックエンドが違います。

setuptools-scmはsetuptools用、hatch-vcsはHatchling用です。 実際、hatch-vcsは内部でsetuptools-scm(v8.2.0以上)を使用しています。 wagtail-reusable-blocksはHatchling(モダンなビルドバックエンド)を採用しているため、hatch-vcsを使用しています。

A: uvはRust製で圧倒的に速いです。

pip/pip-toolsの代替として開発されたツールで、キャッシュなしで8〜10倍、キャッシュありで80〜115倍速いとされています。 コマンド体系もシンプル(uv builduv pip install等)。 既存のpyproject.tomlをそのまま使えるため、移行コストもほぼゼロです。

A: 開発版番号の生成ルールです。

no-guess-devは、次のバージョンを推測せず、現在のタグベースで開発版番号を生成します。 例: v0.1.0タグから3コミット先 → 0.1.1.dev3(次のpatchバージョン + dev番号) デフォルトのguess-next-devだと次のバージョンを推測しますが、誤推測のリスクがあるため明示的な設定を採用しています。

A: はい、使えます。

PyPIに公開しない社内アプリでも、pyproject.tomlでバージョン管理することは可能です。 アプリ内でimportlib.metadata.version("myapp")でバージョン取得し、ログ・管理画面に表示できます。 gitタグとアプリバージョンが一致するため、デプロイ時のトラブルシューティングが楽になります。

参考: hatch-vcs - GitHub

もっと詳しく知りたい人へ(参考文献)

関連トピック

コメント (0)

まだコメントはありません。最初のコメントを残しませんか?

コメントを投稿

メールアドレスが公開されることはありません。必須項目には * が付いています