プラグイン開発ガイド¶
AutoShip-CLI のプラグインシステムは pluggy に基づいています。autoship.hookspec.AutoShipHookSpec で定義されたフックを実装し、プラグインオブジェクトを autoship.plugins entry point に登録するだけです。
autoship-sdkを使用してプラグイン開発を簡素化することを推奨します。 ベースクラス、デコレータ、テストツールが提供されています。
autoship-sdk の使用(推奨)¶
SDK をインストール:
最小プラグインの例:
from autoship_sdk import Plugin, hook
from autoship.core.context import CommandContext
class MyPlugin(Plugin):
@hook
def pre_commit(self, context: CommandContext) -> None:
print(f"About to commit in {context.project_root}")
pyproject.toml で登録:
プラグインのテスト¶
PluginTestHarness は分離された Hook 呼び出し環境を提供します:
from autoship_sdk.testing import PluginTestHarness
from my_plugin.plugin import MyPlugin
def test_pre_commit():
harness = PluginTestHarness()
harness.register(MyPlugin())
ctx = harness.make_context("commit")
results = harness.call("pre_commit", ctx)
assert results == [None]
プロジェクトスキャフォールド¶
from autoship_sdk import create_plugin
from pathlib import Path
create_plugin(
target_dir=Path("./autoship-my-plugin"),
plugin_name="my-plugin",
description="My first AutoShip plugin",
)
これにより pyproject.toml、README.md、プラグインのソースコード、テスト構造を含む完全なプロジェクトが生成されます。
CommandContext¶
すべてのフックは不変の CommandContext を受け取ります:
@dataclass(frozen=True)
class CommandContext:
command: str # 現在のコマンド名(例:"verify")
project_root: Path # プロジェクトルートディレクトリ
config: AppConfig # 読み込み済みの設定オブジェクト
verbose: bool = False # 詳細出力が有効かどうか
dry_run: bool = False # プレビューのみかどうか
yes: bool = False # 確認をスキップするかどうか
trace_id: str = "" # 監査トレース ID
extras: dict[str, Any] = field(default_factory=dict) # コマンドの追加パラメータ
コマンドは extras を通じてフックに追加情報を渡すことができます。例えば verify コマンドは {"verify_command": ..., "fix": ...} を渡します。
最小プラグインの例¶
my_plugin.py を作成:
from autoship.core.context import CommandContext
from autoship.hookspec import hookimpl
class MyPlugin:
@hookimpl
def pre_commit(self, context: CommandContext) -> None:
print(f"About to commit in {context.project_root}")
plugin = MyPlugin()
entry_points での登録¶
プラグインを独立した Python パッケージとしてパッケージ化し、pyproject.toml で登録することを推奨します:
my_plugin:plugin はフックメソッドを含むプラグインオブジェクトを指します。プラグインオブジェクトを返すファクトリ関数を指すこともできます:
AutoShip は起動時にすべての autoship.plugins entry points を自動的に発見して読み込みます。
完全な例:on_error 修正提案¶
以下のプラグインは verify --fix の失敗時に修正提案を返します:
from autoship.core.context import CommandContext
from autoship.core.fix import FixSuggestion
from autoship.hookspec import hookimpl
class FixOnVerifyPlugin:
@hookimpl
def on_error(self, context: CommandContext, error: Exception) -> FixSuggestion | None:
if context.command != "verify":
return None
message = str(error)
if "ImportError" not in message:
return None
return FixSuggestion(
description="Missing dependency detected; run `uv pip install -e .`",
patch="",
)
plugin = FixOnVerifyPlugin()
インストールと検証¶
- プラグインパッケージをインストール:
- プラグインが読み込まれているか確認:
- ユニットテストを実行(本リポジトリの
examples/custom-pluginを参照)。
リポジトリの例¶
examples/custom-plugin を参照してください。この例は以下を実装しています:
pre_commit:コミット前にプロジェクトルートディレクトリのTODOファイルをスキャンして警告。on_error:verifyが失敗しユーザーが--fixを有効にした場合、FixSuggestionを返す。
ベストプラクティス¶
- 関心のある例外のみキャッチ:
on_errorで重要な例外を飲み込まないでください。Noneを返して他のプラグインに処理を委ねます。 - 副作用を避ける:
pre_*/post_*フックは軽量にし、複雑な操作は非同期またはサブプロセスでの実行を推奨します。 dry_runとyesを尊重:ファイルシステムやリモート状態を変更する前にこれらのフラグを確認してください。- 機密情報を漏洩しない:
context.configから資格情報を読み取る際、ログへの出力を避けてください。 - 型アノテーションを使用:静的チェック(pyright / mypy)がしやすく、コアコードとの一貫性が保たれます。
完全な例:カスタム検証プラグイン¶
以下に「コミット前の機密ファイルチェックプラグイン」を例として、AutoShip プラグインをゼロから開発、テスト、インストールする方法を示します。このプラグインは autoship-sdk を使用し、pre_commit フェーズでプロジェクトルートディレクトリをスキャンし、.env、secrets.json などの機密ファイルが存在する場合はコミットをブロックして明確なメッセージを表示します。
プロジェクト構成¶
autoship-no-secret-plugin/
├── pyproject.toml
├── README.md
└── src/
└── autoship_no_secret_plugin/
├── __init__.py
└── plugin.py
src レイアウトの使用を推奨します。実行時にソースコードディレクトリが誤ってインポートされるのを防ぎ、パッケージツール(hatchling、setuptools など)がパッケージパスを正しく解決できます。
pyproject.toml の設定¶
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "autoship-no-secret-plugin"
version = "0.1.0"
description = "Block commits when common secret files are present"
requires-python = ">=3.10"
dependencies = [
"autoship>=0.2.0b1",
"autoship-sdk>=0.1.0b1",
]
[project.entry-points."autoship.plugins"]
no_secret = "autoship_no_secret_plugin.plugin:register"
説明:
[build-system]はhatchlingを使用します。軽量で追加のMANIFEST.inが不要です。[project.entry-points."autoship.plugins"]はプラグインを AutoShip のプラグイン名前空間に登録します。no_secretはプラグイン ID、autoship_no_secret_plugin.plugin:registerはファクトリ関数を指します。- 依存関係に
autoshipとautoship-sdkの両方を宣言します。前者は型とランタイムの互換性に、後者はPluginベースクラスとhookデコレータに使用します。
プラグインコード¶
src/autoship_no_secret_plugin/__init__.py:
src/autoship_no_secret_plugin/plugin.py:
from __future__ import annotations
from autoship_sdk import Plugin, hook
from autoship.core.context import CommandContext
class NoSecretPlugin(Plugin):
"""Scan the project root for sensitive files before each commit."""
SENSITIVE_PATTERNS = (".env", "secrets.json", "credentials.json")
@hook
def pre_commit(self, context: CommandContext) -> None:
"""Raise an error if a sensitive file is found in the project root."""
# dry-run モードでは警告のみで、実際のワークフローはブロックしない
if context.dry_run:
return
for name in self.SENSITIVE_PATTERNS:
path = context.project_root / name
if path.exists():
raise RuntimeError(
f"[no-secret-plugin] Blocked commit: sensitive file "
f"'{path.name}' exists. Add it to .gitignore or remove it."
)
def register() -> NoSecretPlugin:
"""Factory used by the ``autoship.plugins`` entry point."""
return NoSecretPlugin()
ポイント:
autoship_sdk.Pluginを継承し@hookでフックを登録する方が、hookimplを直接操作するよりも簡潔です。Path.cwd()ではなくcontext.project_rootでファイルを特定することで、サブディレクトリでautoship commitを実行した場合でも正しくチェックできます。context.dry_runをチェックし、プレビューモードでは例外をスローしないようにします。ブロッキング系のチェックでは dry-run は通常サイレントにスキップすべきです。SystemExitではなく具体的な例外型(RuntimeErrorなど)を使用し、AutoShip のエラー処理と監査ログでキャプチャして表示できるようにします。
テスト例¶
プラグインディレクトリに tests/test_plugin.py を作成:
import pytest
from autoship_sdk.testing import PluginTestHarness
from autoship_no_secret_plugin.plugin import NoSecretPlugin
def test_pre_commit_allows_clean_project():
harness = PluginTestHarness()
harness.register(NoSecretPlugin())
ctx = harness.make_context("commit")
results = harness.call("pre_commit", ctx)
assert results == [None]
def test_pre_commit_blocks_secret_file():
harness = PluginTestHarness()
harness.register(NoSecretPlugin())
ctx = harness.make_context("commit")
# PluginTestHarness の project_root は一時ディレクトリ
secret = ctx.project_root / ".env"
secret.write_text("SECRET=1\n")
with pytest.raises(RuntimeError, match=".env"):
harness.call("pre_commit", ctx)
テスト戦略:
- 正方向のユースケースで機密ファイルがない場合にプラグインがエラーを出さないことを確認。
- 負方向のユースケースで
project_rootの一時ディレクトリに.envを書き込み、プラグインがファイル名を含む例外をスローすることを検証。 PluginTestHarnessを使用して Hook 呼び出し環境を分離し、実際のプロジェクトへの影響を回避。
テストの実行:
インストールと検証¶
-
編集可能モードでプラグインをインストール:
-
AutoShip がプラグインを読み込んだか確認:
以下のような出力が表示されるはずです:
-
任意のプロジェクトで dry-run モードをテスト:
プラグインは
dry_runの場合は直接 return するため、ブロックされません。 -
実際のブロックをトリガー:
AutoShip が
pre_commitフェーズで例外をキャプチャし、以下を表示することが期待されます: -
クリーンアップ後に再度コミット:
本リポジトリの例との関係¶
本リポジトリの examples/custom-plugin/ も pre_commit(TODO ファイルのチェック)と on_error(FixSuggestion の返却)を実装しています。両者の違いはビジネスロジックのみです:
custom-pluginは失敗時に修正提案を出す方法を学ぶのに適しています。- この節の
no-secret-pluginは軽量な事前チェックを行い、dry_runを尊重し、具体的な例外を使用し、プロジェクトパスに依存しない方法を学ぶのに適しています。
プラグイン開発時は、まず autoship-sdk の Plugin + @hook パターンを使用することを推奨します。シンプルな関数型プラグインが必要な場合のみ、hookimpl の直接使用を検討してください。