diff --git a/docs-tech/gitea-openclaw-dev-process/index.md b/docs-tech/gitea-openclaw-dev-process/index.md deleted file mode 100644 index 624a234..0000000 --- a/docs-tech/gitea-openclaw-dev-process/index.md +++ /dev/null @@ -1,111 +0,0 @@ ---- -sidebar_position: 2 -title: Gitea × OpenClaw 連携の開発プロセス実践ガイド -description: Issue/PR Webhook連携でAI開発を回すための実運用プロセス(ブランチ戦略、レビュー、マージ、再発防止) -hide_table_of_contents: false -displayed_sidebar: null ---- - -# Gitea × OpenClaw 連携の開発プロセス実践ガイド - -## はじめに - -Gitea Webhook で OpenClaw を呼び出す構成は便利ですが、運用ルールが曖昧だとすぐ壊れます。 - -この記事では、実際に運用しながら整えた **Issue/PR 駆動の開発プロセス** をまとめます。 - -## 目的 - -- Issue を起点に AI が修正提案・実装を進める -- PR でレビューし、問題なければ main へ反映 -- ブランチ運用・反映手順を毎回明示し、Git に不慣れでも迷わない - -## 全体フロー - -1. Gitea で Issue を作成 -2. Webhook Bridge が OpenClaw にタスク転送 -3. 修正ブランチで実装・検証 -4. PR 作成 -5. PR Webhook で最終レビュー -6. main へマージ -7. Issue に進捗コメント(手順付き) - -## 運用ルール(重要) - -### 1) 変更は必ず作業ブランチで - -- `fix/issue-<番号>-<要約>` -- `feat/issue-<番号>-<要約>` -- `docs/issue-<番号>-<要約>` - -### 2) Issue コメントに毎回この3点を記載 - -- ブランチ切替コマンド -- 検証コマンド -- main へのマージコマンド - -### 3) PR イベントも Webhook で処理 - -`issues` だけでなく `pull_request` も受ける。 -対象アクションは最低限以下を推奨: - -- `opened` -- `reopened` -- `synchronize` - -## 実用テンプレ(そのまま使える) - -### ブランチ切替 - -```bash -git fetch origin -git switch fix/issue-123-xxx -# 古いgitなら: git checkout fix/issue-123-xxx -``` - -### 検証 - -```bash -python3 -m py_compile src/*.py app/*.py -python3 app/web_ui.py --port 7860 --config config/config.yaml -``` - -### main へ反映 - -```bash -git switch main -git pull origin main -git merge --no-ff fix/issue-123-xxx -git push origin main -``` - -## 破綻しやすいポイントと対策 - -### Webhook が `EOF` で失敗 - -- 原因の多くは受信サーバー側の例外落ち -- `do_POST` 全体を `try/except` で囲み、必ず HTTP 応答を返す - -### 別機能の修正で Webhook が壊れる - -- ブリッジ機能を分離(例: Gitea用 / Discord用を別リポ・別サービス) -- systemd サービスも分離 - -### ローカル設定ファイルが pull を壊す - -- `config.yaml.example` を追跡 -- `config.yaml` は `.gitignore` - -## まとめ - -Gitea × OpenClaw 連携は、技術そのものより **運用ルールの明文化** が効きます。 - -- ブランチ命名を固定 -- Issue コメントのテンプレを固定 -- PR Webhook まで含める -- 失敗しやすい箇所を分離・自動化 - -この4つを守るだけで、連携はかなり安定します。 - ---- -*この記事は2026年2月時点の情報です。* diff --git a/docs-tech/gitea-webhook-ai-review/index.md b/docs-tech/gitea-webhook-ai-review/index.md index 311724d..4afa079 100644 --- a/docs-tech/gitea-webhook-ai-review/index.md +++ b/docs-tech/gitea-webhook-ai-review/index.md @@ -1,7 +1,7 @@ --- sidebar_position: 2 title: ローカルGitea × Webhook連携で、AIとの開発をもっと楽にしよう! -description: GiteaのWebhookとPythonブリッジを使って、Issue作成だけでAIが自動コードレビュー&修正してくれる仕組みを作る +description: GiteaのIssue/PR WebhookとOpenClawを連携し、壊れにくい自動レビュー開発フローを構築する実践ガイド hide_table_of_contents: false displayed_sidebar: null --- @@ -10,284 +10,87 @@ displayed_sidebar: null ## はじめに -「Issue を立てるだけで、AI が自動でコードレビューして修正まで出してくれたらなぁ…」 +Issueを作ったらAIがレビューして、PRが来たらAIが最終チェックして、最後はmainに安全に反映する。 -そう思ったことはありませんか? 実はローカルの Gitea と Webhook、そして AI エージェントを繋げるだけで、わりと簡単にそんな世界が実現できます。 +この記事は、Gitea + OpenClaw 連携を **実運用で壊れにくく回すための最新版プロセス** をまとめたものです。 -この記事では、**Gitea で Issue が作成されたら自動で AI コードレビューが走る仕組み**を、Python の標準ライブラリだけで作った話を紹介します。 +## できること -## 全体像 +- Issue作成で AI レビュー/修正タスクを起動 +- Pull Request 作成でも AI レビューを起動 +- ブランチ運用・反映手順をテンプレ化して Git 不慣れでも回せる +- ブリッジを systemd 常駐して安定運用 -構成はシンプルです。 +## 最新アーキテクチャ ``` -┌─────────┐ Webhook (HTTP POST) ┌──────────────┐ -│ Gitea │ ──────────────────────────→ │ bridge.py │ -│ (別ホスト) │ │ (Python HTTP) │ -└─────────┘ └──────┬───────┘ - │ - openclaw agent コマンド - │ - ┌──────▼───────┐ - │ OpenClaw │ - │ (AI Agent) │ - └──────┬───────┘ - │ - code-review-loop スキル - │ - ┌──────▼───────┐ - │ レビュー結果を │ - │ Issue にコメント │ - └──────────────┘ +┌─────────┐ Webhook (issues / pull_request) ┌────────────────────┐ +│ Gitea │ ────────────────────────────────────→ │ gitea-webhook-bridge │ +└─────────┘ │ (bridge.py) │ + └────────┬──────────┘ + │ + openclaw agent + │ + code-review-loop + │ + Issueコメント ``` -**流れ:** +## Webhook対象イベント -1. Gitea で Issue が作成される -2. システム Webhook が `bridge.py` に POST を送る -3. `bridge.py` が HMAC 署名を検証し、`openclaw agent` コマンドを実行 -4. OpenClaw が `code-review-loop` スキルでリポジトリをレビュー&修正 -5. 結果が Gitea の Issue にコメントとして返ってくる +現在の推奨設定は以下です。 -## 前提 +- `issues`: + - `opened` +- `pull_request`: + - `opened` + - `reopened` + - `synchronize` -この記事では以下が揃っている前提で進めます: +`pull_request` 未対応だと、PRで `OK (ignored event)` になって自動レビューされません。 -- **ローカル Gitea** がセットアップ済み(別ホストでもOK) -- **OpenClaw**(AI エージェントフレームワーク)がセットアップ済み -- bridge.py を動かすホストに Python 3 がある +## bridge.py の要点 -:::tip -OpenClaw は AI エージェントを CLI やサービスとして動かせるフレームワークです。`openclaw agent --message "..."` で 1 回限りのタスクを実行できます。 +- `X-Gitea-Signature` で HMAC 検証 +- `/webhook/gitea` で `issues` / `pull_request` を分岐処理 +- `openclaw agent` を非同期で起動 +- Webhookハンドラで例外が起きても **必ずHTTPレスポンスを返す**(EOF対策) + +:::warning +`EOF` が出る場合、受信サーバーが例外落ちしてレスポンス返せていないケースが多いです。 +`do_POST` 全体を `try/except` で保護し、`500` を返すようにしてください。 ::: -## bridge.py の実装 +## 運用で効いたルール -ポイントは **Python 標準ライブラリのみ** で作っていること。`pip install` 不要です。 +### 1) ブランチ命名を固定 -```python title="bridge.py" -#!/usr/bin/env python3 -"""Gitea Webhook Bridge - forwards new Issues to OpenClaw for auto code review.""" +- `fix/issue-<番号>-<要約>` +- `feat/issue-<番号>-<要約>` +- `docs/issue-<番号>-<要約>` -import hashlib -import hmac -import json -import logging -import os -import subprocess -import sys -import threading -from http.server import HTTPServer, BaseHTTPRequestHandler +### 2) Issueコメントに毎回添える3点 -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s [%(levelname)s] %(message)s", - stream=sys.stdout, -) -log = logging.getLogger("bridge") +- ブランチ切替コマンド +- 検証コマンド +- main へのマージコマンド -CONFIG_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.json") +### 3) 機能を分離する +Webhook橋渡しを1つに詰め込みすぎると壊れます。 -def load_config(): - with open(CONFIG_PATH) as f: - return json.load(f) +実運用では以下のように分離すると安全です。 +- `gitea-webhook-bridge`(Issue/PR連携専用) +- `discord-notify-bridge`(通知専用) -def verify_signature(secret: str, body: bytes, signature: str) -> bool: - mac = hmac.new(secret.encode(), body, hashlib.sha256) - return hmac.compare_digest(mac.hexdigest(), signature) +サービスも別systemdに分離。 - -def run_agent(task: str, label: str): - """Run openclaw agent with a message instructing it to spawn a sub-agent.""" - import time - session_id = f"gitea-webhook-{label}-{int(time.time())}" - message = ( - f"以下のタスクを sessions_spawn (mode=run) で実行してください。" - f"自分では処理せず、必ず sessions_spawn tool を使って" - f"サブエージェントに委譲してください。\n\n" - f"label: {label}\n\n" - f"task:\n{task}" - ) - cmd = [ - "openclaw", "agent", - "--session-id", session_id, - "--message", message, - "--timeout", "120", - ] - log.info("Running openclaw agent for label=%s", label) - try: - result = subprocess.run( - cmd, - capture_output=True, - text=True, - timeout=180, - ) - if result.returncode == 0: - log.info("Agent completed for %s: %s", label, result.stdout[:200]) - else: - log.error( - "Agent failed for %s: rc=%d stderr=%s", - label, result.returncode, result.stderr[:200], - ) - except subprocess.TimeoutExpired: - log.error("Agent timed out for %s", label) - except Exception as e: - log.error("Agent error for %s: %s", label, e) - - -class WebhookHandler(BaseHTTPRequestHandler): - def do_POST(self): - if self.path != "/webhook/gitea": - self.send_response(404) - self.end_headers() - return - - content_length = int(self.headers.get("Content-Length", 0)) - body = self.rfile.read(content_length) - - # Signature verification - config = load_config() - secret_env = config.get("webhook_secret_env", "GITEA_WEBHOOK_SECRET") - secret = os.environ.get(secret_env, "") - if secret: - sig = self.headers.get("X-Gitea-Signature", "") - if not verify_signature(secret, body, sig): - log.warning("Signature verification failed") - self.send_response(403) - self.end_headers() - self.wfile.write(b"Invalid signature") - return - - # Only process issue events - event = self.headers.get("X-Gitea-Event", "") - if event != "issues": - log.info("Ignoring event: %s", event) - self.send_response(200) - self.end_headers() - self.wfile.write(b"OK (ignored event)") - return - - try: - payload = json.loads(body) - except json.JSONDecodeError: - self.send_response(400) - self.end_headers() - return - - action = payload.get("action", "") - if action != "opened": - log.info("Ignoring action: %s", action) - self.send_response(200) - self.end_headers() - self.wfile.write(b"OK (ignored action)") - return - - # Extract info - issue = payload.get("issue", {}) - repo = payload.get("repository", {}) - repo_full_name = repo.get("full_name", "") - clone_url = repo.get("clone_url", "") - number = issue.get("number", 0) - title = issue.get("title", "") - body_text = issue.get("body", "") - html_url = issue.get("html_url", "") - - # Check ignore list - ignore_repos = config.get("ignore_repos", []) - if repo_full_name in ignore_repos: - log.info("Ignoring repo: %s", repo_full_name) - self.send_response(200) - self.end_headers() - self.wfile.write(b"OK (ignored repo)") - return - - # Build task - task = ( - f"Gitea Issue #{number} on {repo_full_name}: {title}\n\n" - f"{body_text}\n\n" - f"リポジトリ: {clone_url}\n" - f"Issue URL: {html_url}\n\n" - f"code-review-loop スキルに従ってレビュー&修正してください。" - ) - label = f"gitea-review-{repo_full_name.replace('/', '-')}-{number}" - - log.info("Spawning agent for %s#%d: %s", repo_full_name, number, title) - - # Run in background thread - threading.Thread( - target=run_agent, - args=(task, label), - daemon=True, - ).start() - - self.send_response(200) - self.end_headers() - self.wfile.write(b"OK") - - def do_GET(self): - if self.path == "/health": - self.send_response(200) - self.end_headers() - self.wfile.write(b"OK") - return - self.send_response(404) - self.end_headers() - - def log_message(self, format, *args): - pass - - -def main(): - config = load_config() - bind = config.get("bind", "127.0.0.1") - port = config.get("port", 9876) - server = HTTPServer((bind, port), WebhookHandler) - log.info("Listening on %s:%d", bind, port) - try: - server.serve_forever() - except KeyboardInterrupt: - log.info("Shutting down") - server.server_close() - - -if __name__ == "__main__": - main() -``` - -### 設定ファイル - -```json title="config.json" -{ - "bind": "0.0.0.0", - "port": 9876, - "webhook_secret_env": "GITEA_WEBHOOK_SECRET", - "ignore_repos": [] -} -``` - -### 環境変数 - -```bash title=".env" -GITEA_WEBHOOK_SECRET=ここにWebhookシークレットを設定 -``` - -### コードのポイント - -- **HMAC-SHA256 署名検証**: Gitea が送ってくる `X-Gitea-Signature` ヘッダーを検証。なりすまし防止 -- **Issue の `opened` アクションのみ処理**: 編集やクローズでは発火しない -- **バックグラウンドスレッド**: Webhook のレスポンスを即座に返し、エージェント実行は非同期 -- **`ignore_repos`**: レビュー不要なリポジトリを除外できる -- **`/health` エンドポイント**: 監視用のヘルスチェック - -## systemd サービス化 - -常駐させるために systemd ユニットを作ります。 +## systemd の基本 ```ini title="/etc/systemd/system/gitea-webhook-bridge.service" [Unit] -Description=Gitea Webhook Bridge +Description=Gitea Webhook Bridge for OpenClaw After=network.target [Service] @@ -309,102 +112,57 @@ sudo systemctl enable --now gitea-webhook-bridge sudo systemctl status gitea-webhook-bridge ``` -## Gitea 側の設定 +## Gitea 側チェックポイント -### ALLOWED_HOST_LIST の設定 - -:::warning 重要 -Gitea はデフォルトで **プライベート IP への Webhook 送信をブロック** します。ローカルネットワーク内で使う場合、この設定が必須です。 -::: - -Gitea の `app.ini` に以下を追加します: +### `ALLOWED_HOST_LIST`(必須) ```ini title="app.ini" [webhook] ALLOWED_HOST_LIST = private ``` -`private` を指定すると、RFC 1918 のプライベートアドレス(`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`)への送信が許可されます。 +### Webhook設定 -設定後は Gitea の再起動が必要です。 +- URL: `http://:9876/webhook/gitea` +- Content-Type: `application/json` +- Secret: bridge側と一致 +- トリガー: `issues`, `pull_request` -### システム Webhook の追加 +## Git弱者向けテンプレ(そのまま使える) -Gitea の **サイト管理 → Webhook** からシステム Webhook を追加します。 - -| 項目 | 設定値 | -|------|--------| -| ターゲット URL | `http://:9876/webhook/gitea` | -| Content Type | `application/json` | -| Secret | `openssl rand -hex 20` で生成した値 | -| トリガー | Issues イベント | - -システム Webhook にすると、**全リポジトリ**に対して自動的に適用されます。個別に設定する手間がなくて楽です。 - -## ハマりポイント 🤦 - -実際に構築してみて踏んだ地雷たちです。 - -### 1. ALLOWED_HOST_LIST を忘れて Webhook が飛ばない - -Gitea の Webhook 配信テストで `Delivery: ...is not allowed` みたいなエラーが出たら、これが原因。`app.ini` に `[webhook]` セクションを追加し忘れていないか確認しましょう。 - -### 2. bind アドレスが 127.0.0.1 のまま - -Gitea が別ホストにある場合、bridge.py が `127.0.0.1` で listen していると当然届きません。`config.json` の `bind` を `0.0.0.0` に変更しましょう。 - -### 3. `openclaw session spawn` は存在しない - -OpenClaw の CLI コマンドを間違えがち。セッションを起動するには `openclaw agent --message "..."` を使います。 - -## 動作テスト - -### 1. bridge.py の起動確認 +### 作業ブランチへ切替 ```bash -# ヘルスチェック -curl http://localhost:9876/health -# → OK +git fetch origin +git switch fix/issue-123-xxx +# 古いGit: git checkout fix/issue-123-xxx ``` -### 2. Gitea から Webhook テスト +### 検証 -Gitea のサイト管理 → Webhook → 作成した Webhook → **テスト配信** ボタンを押します。 - -bridge.py のログに以下のような出力が出ればOK: - -``` -2026-02-25 12:00:00 [INFO] Ignoring event: push +```bash +python3 -m py_compile src/*.py app/*.py ``` -(テスト配信は push イベントなので無視されるのが正常) - -### 3. 実際に Issue を作成 - -適当なリポジトリで Issue を作成してみましょう。bridge.py のログに: +### mainへ反映 +```bash +git switch main +git pull origin main +git merge --no-ff fix/issue-123-xxx +git push origin main ``` -2026-02-25 12:01:00 [INFO] Spawning agent for user/repo#1: テストIssue -2026-02-25 12:01:01 [INFO] Running openclaw agent for label=gitea-review-user-repo-1 -``` - -と出て、しばらくすると Issue にレビュー結果のコメントが付きます 🎉 ## まとめ -Gitea + Webhook + Python ブリッジ + OpenClaw という組み合わせで、**Issue を立てるだけで AI が自動レビューしてくれる仕組み**が作れました。 +Gitea × OpenClaw 連携は、実装より運用設計が重要です。 -構成要素はすべてセルフホストで、外部サービスに依存しないのがポイントです。Python の標準ライブラリだけで書いているので、依存管理も不要。 +- イベント対応(Issue + PR) +- 例外時のHTTP応答保証 +- ブランチ/コメントテンプレの固定 +- 機能分離(リポジトリ・サービス) -### 今後の拡張アイデア - -- **ラベルフィルタ**: 特定のラベル(例: `ai-review`)が付いた Issue だけ処理する -- **PR 自動作成**: レビュー結果を Issue コメントだけでなく、修正 PR として作成する -- **Push イベント対応**: push 時にも自動レビューを走らせる -- **Slack / Discord 通知**: レビュー完了を通知する - -セルフホスト環境で AI を活用した開発フローを作りたい人の参考になれば嬉しいです! +ここを押さえると、連携はかなり安定します。 --- - *この記事は2026年2月時点の情報です。* diff --git a/docs-tech/index.md b/docs-tech/index.md index 393a625..5430ab7 100644 --- a/docs-tech/index.md +++ b/docs-tech/index.md @@ -25,4 +25,3 @@ slug: / - [SearXNGでローカル検索APIを構築する](/tech/searxng-local-search/) - [ローカルGitea × Webhook連携で、AIとの開発をもっと楽にしよう!](/tech/gitea-webhook-ai-review/) - Issue作成だけでAIが自動コードレビューしてくれる仕組み -- [Gitea × OpenClaw 連携の開発プロセス実践ガイド](/tech/gitea-openclaw-dev-process/) - Issue/PR駆動で壊れにくく回す実運用ルール diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 1db8c92..e4fe4d8 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -18,12 +18,6 @@ function HomepageHeader() { function RecentTech() { const posts = [ - { - title: "Gitea × OpenClaw 連携の開発プロセス実践ガイド", - date: "2026-02-27", - tag: "Gitea", - url: "/tech/gitea-openclaw-dev-process", - }, { title: "ローカルGitea × Webhook連携でAI自動コードレビュー", date: "2026-02-26",