-
Notifications
You must be signed in to change notification settings - Fork 0
feat(hooks): add post-push CI monitoring and enable PR AI review #385
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,204 @@ | ||
| #!/usr/bin/env python3 | ||
| """ | ||
| git push後にGitHub Actions CIを監視するPostToolUseフック | ||
|
|
||
| git push 成功後に自動的にCIの状態を確認し、結果を報告します。 | ||
| """ | ||
| import sys | ||
| import json | ||
| import subprocess | ||
| import re | ||
| import time | ||
|
|
||
| # Read input from Claude | ||
| data = json.load(sys.stdin) | ||
|
|
||
| tool_name = data.get("tool_name", "") | ||
| tool_input = data.get("tool_input", {}) or {} | ||
| tool_response = data.get("tool_response", {}) or {} | ||
|
|
||
| # Bashツールでない場合はスキップ | ||
| if tool_name != "Bash": | ||
| sys.exit(0) | ||
|
|
||
| # コマンドを取得 | ||
| command = tool_input.get("command", "").strip() | ||
|
|
||
| # git push コマンドかどうかを判定 | ||
| if not command.startswith("git push"): | ||
| sys.exit(0) | ||
|
|
||
| # --help や --dry-run は除外 | ||
| if "--help" in command or "-h" in command or "--dry-run" in command or "-n" in command: | ||
| sys.exit(0) | ||
|
|
||
| # ツール実行が成功したかチェック | ||
| stdout = tool_response.get("stdout", "") | ||
| stderr = tool_response.get("stderr", "") | ||
| combined_output = stdout + stderr | ||
|
|
||
| # push成功のパターン(新しいブランチ or 既存ブランチへのpush) | ||
| success_patterns = [ | ||
| r"\[new branch\]", | ||
| r"\.\..*->", # abc123..def456 main -> main | ||
| r"set up to track", | ||
| r"Everything up-to-date" | ||
| ] | ||
|
|
||
| # エラーパターン | ||
| error_patterns = [ | ||
| r"error:", | ||
| r"fatal:", | ||
| r"rejected" | ||
| ] | ||
|
|
||
| # エラーがあればスキップ | ||
| for pattern in error_patterns: | ||
| if re.search(pattern, combined_output, re.IGNORECASE): | ||
| sys.exit(0) | ||
|
|
||
| # 成功パターンがなければスキップ | ||
| is_success = any(re.search(p, combined_output) for p in success_patterns) | ||
| if not is_success: | ||
| sys.exit(0) | ||
|
|
||
| print("", file=sys.stderr) | ||
| print("=" * 60, file=sys.stderr) | ||
| print("🚀 Push完了。GitHub Actions CIを確認中...", file=sys.stderr) | ||
| print("=" * 60, file=sys.stderr) | ||
|
|
||
|
|
||
| def get_current_branch(): | ||
| """現在のブランチ名を取得""" | ||
| try: | ||
| result = subprocess.run( | ||
| ["git", "rev-parse", "--abbrev-ref", "HEAD"], | ||
| capture_output=True, | ||
| text=True, | ||
| timeout=10 | ||
| ) | ||
| return result.stdout.strip() | ||
| except Exception: | ||
| return None | ||
|
|
||
|
|
||
| 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 | ||
|
|
||
| except Exception as e: | ||
| print(f"⚠️ CI状態取得エラー: {e}", file=sys.stderr) | ||
| return None | ||
|
|
||
|
|
||
| def watch_ci_run(run_id, timeout_seconds=300): | ||
| """CIの実行を監視(最大5分)""" | ||
| print(f"\n🔄 CI実行を監視中... (最大{timeout_seconds // 60}分)", file=sys.stderr) | ||
|
|
||
| start_time = time.time() | ||
| check_interval = 15 # 15秒ごとにチェック | ||
|
|
||
| while time.time() - start_time < timeout_seconds: | ||
| try: | ||
| result = subprocess.run( | ||
| ["gh", "run", "view", str(run_id), "--json", "status,conclusion,jobs"], | ||
| capture_output=True, | ||
| text=True, | ||
| timeout=30 | ||
| ) | ||
|
|
||
| if result.returncode != 0: | ||
| break | ||
|
|
||
| run_data = json.loads(result.stdout) | ||
| status = run_data.get("status", "") | ||
| conclusion = run_data.get("conclusion", "") | ||
|
|
||
| if status == "completed": | ||
| return conclusion, run_data.get("jobs", []) | ||
|
|
||
| # 進行中の場合は待機 | ||
| elapsed = int(time.time() - start_time) | ||
| print(f" ⏳ {elapsed}秒経過... (status: {status})", file=sys.stderr) | ||
| time.sleep(check_interval) | ||
|
|
||
| except Exception as e: | ||
| print(f"⚠️ 監視エラー: {e}", file=sys.stderr) | ||
| break | ||
|
|
||
| return "timeout", [] | ||
|
|
||
|
|
||
| # メイン処理 | ||
| run = get_latest_run() | ||
|
|
||
| if not run: | ||
| print("⚠️ GitHub Actions ワークフローが見つかりません", file=sys.stderr) | ||
| print(" (CI未設定、またはpush直後でまだ起動していない可能性があります)", file=sys.stderr) | ||
| sys.exit(0) | ||
|
|
||
| run_id = run.get("databaseId") | ||
| workflow_name = run.get("workflowName", run.get("name", "Unknown")) | ||
| status = run.get("status", "") | ||
| conclusion = run.get("conclusion", "") | ||
|
|
||
| print(f"\n📋 ワークフロー: {workflow_name}", file=sys.stderr) | ||
| print(f" Run ID: {run_id}", file=sys.stderr) | ||
| print(f" Status: {status}", file=sys.stderr) | ||
|
|
||
| if status == "completed": | ||
| # 既に完了している場合 | ||
| if conclusion == "success": | ||
| print("\n✅ CI成功!", file=sys.stderr) | ||
| elif conclusion == "failure": | ||
| print("\n❌ CI失敗", file=sys.stderr) | ||
| print(f" 詳細: gh run view {run_id}", file=sys.stderr) | ||
| else: | ||
| print(f"\n⚠️ CI結果: {conclusion}", file=sys.stderr) | ||
| else: | ||
| # 実行中の場合は監視 | ||
| conclusion, jobs = watch_ci_run(run_id) | ||
|
|
||
| if conclusion == "success": | ||
| print("\n✅ CI成功!", file=sys.stderr) | ||
| elif conclusion == "failure": | ||
| print("\n❌ CI失敗", file=sys.stderr) | ||
| # 失敗したジョブを表示 | ||
| failed_jobs = [j for j in jobs if j.get("conclusion") == "failure"] | ||
| if failed_jobs: | ||
| print("\n失敗したジョブ:", file=sys.stderr) | ||
| for job in failed_jobs: | ||
| print(f" - {job.get('name', 'Unknown')}", file=sys.stderr) | ||
| print(f"\n 詳細: gh run view {run_id}", file=sys.stderr) | ||
| elif conclusion == "timeout": | ||
| print("\n⏰ CI監視タイムアウト(まだ実行中)", file=sys.stderr) | ||
| print(f" 詳細: gh run view {run_id} --watch", file=sys.stderr) | ||
| else: | ||
| print(f"\n⚠️ CI結果: {conclusion}", file=sys.stderr) | ||
|
|
||
| print("", file=sys.stderr) | ||
| print("=" * 60, file=sys.stderr) | ||
|
|
||
| # PostToolUseフックは常に成功で終了(ブロックしない) | ||
| sys.exit(0) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
GitHub CLI gh run list --json fields headSha documentation💡 Result:
gh run listsupports JSON output via--json <fields>. One of the valid fields isheadSha(the run’s commit SHA). (cli.github.com)Docs / reference
gh run listmanual page (includes the JSON fields list, includingheadSha). (cli.github.com)--json,--jq, and--templateformatting docs. (cli.github.com)Example
Citations:
🌐 Web query:
GitHub CLI gh run list default ordering newest first chronological💡 Result:
gh run listshows recent workflow runs and (in practice) the output is newest-first (reverse chronological). The command has no--sort/--orderflags, 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:
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 1right 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 fetchesheadShabut 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)
📝 Committable suggestion
🧰 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:
subprocesscall: 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
elseblock(TRY300)
🤖 Prompt for AI Agents