feat(hooks): add post-push CI monitoring and enable PR AI review - #385
Conversation
## 追加されたHooks ### PostToolUse - `post_git_push_ci.py`: git push後にGitHub Actions CIを自動監視 - 最大5分間CIの実行を監視 - 成功/失敗の結果を報告 - ブロックはしない(情報提供のみ) - `post_pr_ai_review.py`: PR作成後のCodex/Geminiレビュー(有効化) - Codex CLIまたはGemini CLIでコードレビューを実行 - 正確性、パフォーマンス、セキュリティ、保守性を評価 ## 設定ファイル更新 - `.devcontainer/claude-settings.local.json`: DevContainer用PostToolUse設定追加 - `.claude/hooks/README.md`: 新hookのドキュメント追加 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughIntroduces a new GitHub Actions CI monitoring hook that observes git push operations, validates successful completion, queries the current Git branch, fetches the latest workflow run via gh CLI, and polls its status for up to 5 minutes with periodic progress reporting. Changes
Sequence DiagramsequenceDiagram
actor User
participant Claude
participant Bash as Bash Tool
participant Hook as post_git_push_ci.py
participant Git as Git
participant GitHub as GitHub API<br/>(via gh CLI)
User->>Claude: Request git push operation
Claude->>Bash: Execute git push command
Bash->>GitHub: Push code
Bash-->>Hook: Post-action trigger
Hook->>Bash: Validate command (is git push?)
Bash-->>Hook: Command confirmed
Hook->>Git: Query current branch
Git-->>Hook: Return branch name
Hook->>GitHub: Fetch latest workflow run
GitHub-->>Hook: Return workflow run
rect rgba(173, 216, 230, 0.5)
Note over Hook: Polling Loop<br/>(max 5 min, every 15 sec)
Hook->>GitHub: Check workflow status
GitHub-->>Hook: Return status
alt Status: Success
Hook-->>User: Report success
else Status: Failure
Hook-->>User: Report failure + details
else Status: Still Running
Hook->>Hook: Wait & retry
end
end
Hook-->>User: Exit with code 0 (non-blocking)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
PR レビュー: Post-Push CI Monitoring HookこのPRを確認しました。git push後のCI監視を自動化する機能追加です。全体的に良い実装ですが、いくつかの改善提案があります。 ✅ 良い点
🔍 コード品質に関する指摘1. 潜在的な競合状態 (.claude/hooks/post_git_push_ci.py:93)固定の3秒待機では、ワークフロー起動が遅い場合に前回のpushのCIを取得する可能性があります。 提案: headSha を使用してpush後のcommit SHAと照合する 2. returnコード検証の不足 (.claude/hooks/post_git_push_ci.py:80)git rev-parse が失敗した場合でも stdout をそのまま返します。 提案: returncode を確認してから結果を返す 3. 例外ハンドリングが広すぎる (.claude/hooks/post_git_push_ci.py:110-112)すべての例外を捕捉すると、予期しないエラーの詳細が失われます。 提案: 特定の例外のみを捕捉するか、エラーの種類を明示する ⚡ パフォーマンスに関する指摘4. 最大5分の同期待機 (.claude/hooks/post_git_push_ci.py:115-150)watch_ci_run() が最大300秒(5分)ブロックします。これはPostToolUseフックとして長すぎる可能性があります。 提案: タイムアウトを短縮するか、環境変数で設定可能にする 🔒 セキュリティに関する指摘5. コマンドインジェクションのリスク低減✅ すべての subprocess.run() 呼び出しでリスト形式のコマンド引数を使用しており、セキュリティ面は良好です。 🧪 テストカバレッジに関する指摘6. ユニットテストが存在しないこのフックスクリプトに対するテストが含まれていません。CLAUDE.mdの要件(70%+カバレッジ)を満たしていません。 提案: 以下のテストケースを追加
📝 ドキュメントに関する指摘7. README.mdの設定例の不一致実際の設定ファイルのパスが /home/vscode/.claude/hooks/post_git_push_ci.py なのに対し、READMEの例では相対パスになっています。 📊 総合評価
✅ マージ可否以下のクリティカルな問題を修正後、マージを推奨します:
テスト追加なしでのマージはCLAUDE.mdの品質基準(70%+カバレッジ)に抵触します。 機能自体は有用で、実装も概ね良好です。テストを追加すれば品質基準を満たせます。 |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In @.claude/hooks/post_git_push_ci.py:
- Around line 85-109: get_latest_run may return a stale workflow because it
queries the latest run once; change it to retry for up to 30 seconds and return
the run whose headSha equals the current HEAD SHA. Specifically, inside
get_latest_run (which already calls get_current_branch and parses gh run list
output), loop with a short sleep (e.g., 1–3s) until a timeout, call gh run list
(increase --limit to fetch several recent runs) each iteration, parse runs and
search for a run where run["headSha"] == current_head_sha (obtain
current_head_sha via git rev-parse HEAD), and return that matched run; if
timeout elapses return None. Ensure subprocess.run error handling and timeouts
remain intact and keep existing return shapes.
| def get_latest_run(): | ||
| """最新のworkflow runを取得""" | ||
| try: | ||
| branch = get_current_branch() | ||
| if not branch: | ||
| return None | ||
|
|
||
| # 少し待ってからCIの状態を確認(ワークフロー起動に時間がかかる場合があるため) | ||
| time.sleep(3) | ||
|
|
||
| result = subprocess.run( | ||
| ["gh", "run", "list", "--branch", branch, "--limit", "1", "--json", "databaseId,status,conclusion,name,workflowName,headSha,createdAt"], | ||
| capture_output=True, | ||
| text=True, | ||
| timeout=30 | ||
| ) | ||
|
|
||
| if result.returncode != 0: | ||
| return None | ||
|
|
||
| runs = json.loads(result.stdout) | ||
| if runs: | ||
| return runs[0] | ||
| return None | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
GitHub CLI gh run list --json fields headSha documentation
💡 Result:
gh run list supports JSON output via --json <fields>. One of the valid fields is headSha (the run’s commit SHA). (cli.github.com)
Docs / reference
gh run listmanual page (includes the JSON fields list, includingheadSha). (cli.github.com)- General
--json,--jq, and--templateformatting docs. (cli.github.com)
Example
gh run list --json headShaCitations:
- 1: https://cli.github.com/manual/gh_run_list?utm_source=openai
- 2: https://cli.github.com/manual/gh_run_list?utm_source=openai
- 3: https://cli.github.com/manual/gh_help_formatting?utm_source=openai
🌐 Web query:
GitHub CLI gh run list default ordering newest first chronological
💡 Result:
gh run list shows recent workflow runs and (in practice) the output is newest-first (reverse chronological). The command has no --sort/--order flags, so you can’t switch it to oldest-first directly. [1]
If you need chronological (oldest-first) ordering, fetch JSON and sort client-side, e.g.:
gh run list --limit 100 --json databaseId,createdAt,displayTitle,status,conclusion \
--jq 'sort_by(.createdAt)[]'Notes:
- GitHub’s workflow-runs REST API does not clearly guarantee the default ordering in the docs; it’s commonly observed as
created_atdescending (newest first), but if you need a hard guarantee you must sort yourself. [2][3]
Sources: GitHub CLI manual for gh run list [1]; GitHub REST API workflow-runs docs (ordering not explicitly guaranteed) [2]; GitHub Community discussion on observed default order [3].
🏁 Script executed:
cat -n .claude/hooks/post_git_push_ci.py | head -120Repository: keito4/config
Length of output: 3700
Match CI runs to the pushed commit to avoid stale status.
gh run list --limit 1 right after a push can return the previous run if the new run hasn't started yet, which can misreport CI status. The current code already fetches headSha but doesn't use it for matching. Replace the single-query approach with a retry loop that matches runs by the current HEAD SHA over a 30-second window.
🛠️ Suggested fix (match run by HEAD SHA with a short retry window)
+def get_current_head_sha():
+ """現在のHEAD SHAを取得"""
+ try:
+ result = subprocess.run(
+ ["git", "rev-parse", "HEAD"],
+ capture_output=True,
+ text=True,
+ timeout=10
+ )
+ if result.returncode != 0:
+ return None
+ return result.stdout.strip()
+ except Exception:
+ return None
+
def get_latest_run():
"""最新のworkflow runを取得"""
try:
branch = get_current_branch()
- if not branch:
+ head_sha = get_current_head_sha()
+ if not branch or not head_sha:
return None
- # 少し待ってからCIの状態を確認(ワークフロー起動に時間がかかる場合があるため)
- time.sleep(3)
-
- result = subprocess.run(
- ["gh", "run", "list", "--branch", branch, "--limit", "1", "--json", "databaseId,status,conclusion,name,workflowName,headSha,createdAt"],
- capture_output=True,
- text=True,
- timeout=30
- )
-
- if result.returncode != 0:
- return None
-
- runs = json.loads(result.stdout)
- if runs:
- return runs[0]
- return None
+ deadline = time.time() + 30
+ while time.time() < deadline:
+ time.sleep(3)
+ result = subprocess.run(
+ ["gh", "run", "list", "--branch", branch, "--limit", "20",
+ "--json", "databaseId,status,conclusion,name,workflowName,headSha,createdAt"],
+ capture_output=True,
+ text=True,
+ timeout=30
+ )
+ if result.returncode != 0:
+ return None
+ runs = json.loads(result.stdout)
+ for run in runs:
+ if run.get("headSha") == head_sha:
+ return run
+ return None📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def get_latest_run(): | |
| """最新のworkflow runを取得""" | |
| try: | |
| branch = get_current_branch() | |
| if not branch: | |
| return None | |
| # 少し待ってからCIの状態を確認(ワークフロー起動に時間がかかる場合があるため) | |
| time.sleep(3) | |
| result = subprocess.run( | |
| ["gh", "run", "list", "--branch", branch, "--limit", "1", "--json", "databaseId,status,conclusion,name,workflowName,headSha,createdAt"], | |
| capture_output=True, | |
| text=True, | |
| timeout=30 | |
| ) | |
| if result.returncode != 0: | |
| return None | |
| runs = json.loads(result.stdout) | |
| if runs: | |
| return runs[0] | |
| return None | |
| def get_current_head_sha(): | |
| """現在のHEAD SHAを取得""" | |
| try: | |
| result = subprocess.run( | |
| ["git", "rev-parse", "HEAD"], | |
| capture_output=True, | |
| text=True, | |
| timeout=10 | |
| ) | |
| if result.returncode != 0: | |
| return None | |
| return result.stdout.strip() | |
| except Exception: | |
| return None | |
| def get_latest_run(): | |
| """最新のworkflow runを取得""" | |
| try: | |
| branch = get_current_branch() | |
| head_sha = get_current_head_sha() | |
| if not branch or not head_sha: | |
| return None | |
| deadline = time.time() + 30 | |
| while time.time() < deadline: | |
| time.sleep(3) | |
| result = subprocess.run( | |
| ["gh", "run", "list", "--branch", branch, "--limit", "20", | |
| "--json", "databaseId,status,conclusion,name,workflowName,headSha,createdAt"], | |
| capture_output=True, | |
| text=True, | |
| timeout=30 | |
| ) | |
| if result.returncode != 0: | |
| return None | |
| runs = json.loads(result.stdout) | |
| for run in runs: | |
| if run.get("headSha") == head_sha: | |
| return run | |
| return None |
🧰 Tools
🪛 Ruff (0.14.14)
92-92: Comment contains ambiguous ( (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?
(RUF003)
92-92: Comment contains ambiguous ) (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?
(RUF003)
95-95: subprocess call: check for execution of untrusted input
(S603)
96-96: Starting a process with a partial executable path
(S607)
108-108: Consider moving this statement to an else block
(TRY300)
🤖 Prompt for AI Agents
In @.claude/hooks/post_git_push_ci.py around lines 85 - 109, get_latest_run may
return a stale workflow because it queries the latest run once; change it to
retry for up to 30 seconds and return the run whose headSha equals the current
HEAD SHA. Specifically, inside get_latest_run (which already calls
get_current_branch and parses gh run list output), loop with a short sleep
(e.g., 1–3s) until a timeout, call gh run list (increase --limit to fetch
several recent runs) each iteration, parse runs and search for a run where
run["headSha"] == current_head_sha (obtain current_head_sha via git rev-parse
HEAD), and return that matched run; if timeout elapses return None. Ensure
subprocess.run error handling and timeouts remain intact and keep existing
return shapes.
|
🎉 This PR is included in version 1.58.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
概要
git push後のCI監視とPR作成後のAIレビューを自動化するHooksを追加しました。
追加されたHooks
1.
post_git_push_ci.py(新規)トリガー:
git push成功後機能:
出力例:
2.
post_pr_ai_review.py(有効化)トリガー:
gh pr create成功後機能:
変更ファイル
.claude/hooks/post_git_push_ci.py.claude/hooks/README.md.devcontainer/claude-settings.local.json設定方法
settings.local.jsonに以下を追加:{ "hooks": { "PostToolUse": [ { "matcher": "tool_name == 'Bash'", "hooks": [ { "type": "command", "command": "python3 .claude/hooks/post_git_push_ci.py" } ] }, { "matcher": "tool_name == 'Bash'", "hooks": [ { "type": "command", "command": "python3 .claude/hooks/post_pr_ai_review.py" } ] } ] } }テスト
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation
✏️ Tip: You can customize this high-level summary in your review settings.