Skip to content

Commit 85aa9c3

Browse files
committed
Handle multiple commits in short time
1 parent 3d762af commit 85aa9c3

3 files changed

Lines changed: 239 additions & 25 deletions

File tree

bot.py

Lines changed: 64 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ def _status_command(self, pr) -> str:
259259
pr: Pull request object
260260
"""
261261
try:
262-
status = self.workflow_manager.get_workflow_status(pr.head.sha)
262+
status = self.workflow_manager.get_workflow_status(pr.head.sha, pr.head.ref)
263263
return self._format_status(status)
264264
except Exception as e:
265265
return f"❌ Error getting status: {str(e)}"
@@ -303,29 +303,43 @@ def _format_status(self, status: Dict) -> str:
303303

304304
return "".join(lines)
305305

306-
def post_pr_summary(self, pr_number: int) -> None:
306+
def post_pr_summary(self, pr_number: int, force_update: bool = False) -> None:
307307
"""
308308
Post a summary comment on PR with CI status and helpful information.
309309
310310
Args:
311311
pr_number: PR number
312+
force_update: If True, always update even if comment exists
312313
"""
313314
pr = self.repo.get_pull(pr_number)
314-
status = self.workflow_manager.get_workflow_status(pr.head.sha)
315+
status = self.workflow_manager.get_workflow_status(pr.head.sha, pr.head.ref)
316+
317+
# Check if all checks truly passed (including branch workflows)
318+
all_passed, details = self.workflow_manager.are_all_checks_passed(pr.head.sha, pr.head.ref)
315319

316320
# Check if we already posted a summary
317321
comments = pr.get_issue_comments()
318-
bot_comments = [c for c in comments if c.user.login.endswith("[bot]") and "faneX-ID Bot" in c.body]
322+
bot_comments = [
323+
c for c in comments
324+
if c.user.login.endswith("[bot]")
325+
and ("faneX-ID Bot" in c.body or "🤖 faneX-ID Bot" in c.body)
326+
]
319327

320328
# Format summary
321-
summary = self.comment_handler.create_pr_summary(pr, status)
329+
summary = self.comment_handler.create_pr_summary(pr, status, all_passed, details)
322330

323-
if bot_comments:
324-
# Update existing comment
325-
bot_comments[0].edit(summary)
331+
if bot_comments and not force_update:
332+
# Update existing comment (most recent one)
333+
try:
334+
bot_comments[0].edit(summary)
335+
print(f"✅ Updated existing PR summary comment on PR #{pr_number}")
336+
except Exception as e:
337+
print(f"⚠️ Failed to update comment: {e}, creating new one")
338+
pr.create_issue_comment(summary)
326339
else:
327340
# Create new comment
328341
pr.create_issue_comment(summary)
342+
print(f"✅ Created new PR summary comment on PR #{pr_number}")
329343

330344

331345
def main():
@@ -339,7 +353,7 @@ def main():
339353
if not github_token or not repo_name:
340354
print("Error: FANEX_BOT_TOKEN or GITHUB_TOKEN and GITHUB_REPOSITORY must be set")
341355
sys.exit(1)
342-
356+
343357
# Log which token is being used (for debugging)
344358
if os.getenv("FANEX_BOT_TOKEN"):
345359
print("ℹ️ Using FANEX_BOT_TOKEN - comments will appear as faneX-ID Bot")
@@ -361,28 +375,58 @@ def main():
361375
bot = FanexIDBot(github_token, repo_name)
362376

363377
# Handle different event types
364-
if event.get("action") == "created" and "comment" in event:
378+
event_name = os.getenv("GITHUB_EVENT_NAME", "")
379+
380+
if event_name == "issue_comment" or (event.get("action") == "created" and "comment" in event):
365381
# Issue comment event
366-
comment = event["comment"]
367-
issue = event["issue"]
382+
comment = event.get("comment") or {}
383+
issue = event.get("issue") or {}
368384

369385
# Check if it's a PR (issues and PRs use the same API)
370386
if "pull_request" in issue:
371387
pr_number = issue["number"]
372-
commenter = comment["user"]["login"]
373-
comment_body = comment["body"]
388+
commenter = comment.get("user", {}).get("login", "")
389+
comment_body = comment.get("body", "")
374390

375-
response = bot.process_comment(comment_body, pr_number, commenter)
376-
if response:
377-
# Post response as comment
378-
pr = bot.repo.get_pull(pr_number)
379-
pr.create_issue_comment(response)
391+
# Skip if comment is from a bot (to avoid loops)
392+
if commenter.endswith("[bot]") or commenter == "github-actions[bot]":
393+
print(f"ℹ️ Skipping bot comment from {commenter}")
394+
else:
395+
print(f"📝 Processing comment from {commenter} on PR #{pr_number}")
396+
response = bot.process_comment(comment_body, pr_number, commenter)
397+
if response:
398+
# Post response as comment
399+
pr = bot.repo.get_pull(pr_number)
400+
pr.create_issue_comment(response)
401+
print(f"✅ Posted response to comment on PR #{pr_number}")
402+
else:
403+
print(f"ℹ️ No response needed for comment on PR #{pr_number}")
404+
405+
# Also update the PR summary after processing command
406+
print(f"🔄 Updating PR summary after comment...")
407+
bot.post_pr_summary(pr_number, force_update=True)
380408

381-
elif event.get("action") in ["opened", "synchronize"] and "pull_request" in event:
409+
elif event.get("action") in ["opened", "synchronize", "reopened"] and "pull_request" in event:
382410
# PR opened or updated
383411
pr_number = event["pull_request"]["number"]
412+
print(f"📊 Posting/updating PR summary for PR #{pr_number}")
384413
bot.post_pr_summary(pr_number)
385414

415+
elif event_name == "workflow_call":
416+
# Called from orchestrator - update PR summary
417+
# Try to get PR number from context
418+
pr_number = None
419+
if "pull_request" in event:
420+
pr_number = event["pull_request"]["number"]
421+
elif "issue" in event and "pull_request" in event["issue"]:
422+
pr_number = event["issue"]["number"]
423+
424+
if pr_number:
425+
print(f"📊 Updating PR summary for PR #{pr_number} (workflow_call)")
426+
bot.post_pr_summary(pr_number, force_update=True)
427+
else:
428+
print("⚠️ Could not determine PR number from workflow_call event")
429+
386430

387431
if __name__ == "__main__":
388432
main()

comment_handler.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@ def __init__(self, repo: Repository):
1919
"""
2020
self.repo = repo
2121

22-
def create_pr_summary(self, pr: PullRequest, status: Dict) -> str:
22+
def create_pr_summary(self, pr: PullRequest, status: Dict, all_passed: bool = False, details: Dict = None) -> str:
2323
"""
2424
Create a summary comment for a PR.
2525
2626
Args:
2727
pr: Pull request object
2828
status: Workflow status dictionary
29+
all_passed: Whether all checks truly passed (including branch workflows)
30+
details: Additional details about check status
2931
3032
Returns:
3133
Formatted markdown comment
@@ -37,6 +39,35 @@ def create_pr_summary(self, pr: PullRequest, status: Dict) -> str:
3739
f"**Branch:** `{pr.head.ref}` → `{pr.base.ref}`\n",
3840
]
3941

42+
# Add overall status
43+
if details:
44+
if details.get("truly_all_passed"):
45+
lines.append("\n✅ **All checks passed successfully!**\n\n")
46+
else:
47+
lines.append("\n⚠️ **Some checks are still running or have failed.**\n\n")
48+
49+
if details.get("running_workflows"):
50+
lines.append(f"⏳ **{len(details['running_workflows'])} workflow(s) still running:**\n")
51+
for wf in details["running_workflows"][:10]:
52+
lines.append(f"- {wf['name']}: {wf['status']}\n")
53+
if len(details["running_workflows"]) > 10:
54+
lines.append(f"- ... and {len(details['running_workflows']) - 10} more\n")
55+
lines.append("\n")
56+
57+
if details.get("failed_workflows"):
58+
lines.append(f"❌ **{len(details['failed_workflows'])} workflow(s) failed:**\n")
59+
for wf in details["failed_workflows"][:10]:
60+
lines.append(f"- {wf['name']}: {wf.get('conclusion', 'failure')}\n")
61+
if len(details["failed_workflows"]) > 10:
62+
lines.append(f"- ... and {len(details['failed_workflows']) - 10} more\n")
63+
lines.append("\n")
64+
65+
if details.get("pending_checks"):
66+
lines.append(f"⏳ **{len(details['pending_checks'])} check(s) still running**\n\n")
67+
68+
if details.get("failed_checks"):
69+
lines.append(f"❌ **{len(details['failed_checks'])} check(s) failed**\n\n")
70+
4071
# Add CI/CD status
4172
if status.get("workflows"):
4273
lines.append("\n### 📊 CI/CD Status\n\n")
@@ -81,7 +112,7 @@ def create_pr_summary(self, pr: PullRequest, status: Dict) -> str:
81112
lines.append(f"\n**Summary:** {success}/{total} passed, {failed} failed, {in_progress} in progress\n")
82113

83114
# Add helpful commands if there are failures
84-
if failed > 0:
115+
if failed > 0 or (details and (details.get("failed_workflows") or details.get("failed_checks"))):
85116
lines.append("\n### 🔧 Quick Actions\n\n")
86117
lines.append("If workflows failed, you can retry them:\n\n")
87118
lines.append("- `/retry` - Retry all failed workflows\n")

workflow_manager.py

Lines changed: 142 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"""
33
Workflow Manager - Handles GitHub Actions workflow operations.
44
"""
5-
from typing import Dict, List, Optional
5+
from typing import Dict, List, Optional, Tuple
66
from github import Github
77
from github.GithubException import GithubException
88

@@ -49,18 +49,46 @@ def get_workflow_runs(self, sha: str) -> List[Dict]:
4949

5050
return workflow_runs
5151

52-
def get_workflow_status(self, sha: str) -> Dict:
52+
def get_workflow_status(self, sha: str, branch: str = None) -> Dict:
5353
"""
54-
Get status summary of all workflows for a commit.
54+
Get status summary of all workflows for a commit and optionally branch.
5555
5656
Args:
5757
sha: Commit SHA
58+
branch: Optional branch name to also check workflows for the branch
5859
5960
Returns:
6061
Dictionary with workflow status information
6162
"""
63+
# Get runs for the specific SHA
6264
runs = self.get_workflow_runs(sha)
6365

66+
# Also get runs for the branch if provided
67+
# This is important because auto-fix commits might not trigger all workflows,
68+
# but workflows from earlier commits in the PR might still be running
69+
if branch:
70+
try:
71+
branch_runs = self.repo.get_workflow_runs(branch=branch)
72+
branch_workflow_runs = []
73+
for run in branch_runs:
74+
branch_workflow_runs.append({
75+
"id": run.id,
76+
"name": run.name,
77+
"status": run.status,
78+
"conclusion": run.conclusion,
79+
"workflow_id": run.workflow_id,
80+
"created_at": run.created_at.isoformat() if run.created_at else None,
81+
"url": run.html_url,
82+
})
83+
# Combine and deduplicate by run ID
84+
all_runs = {run["id"]: run for run in runs}
85+
for run in branch_workflow_runs:
86+
if run["id"] not in all_runs:
87+
all_runs[run["id"]] = run
88+
runs = list(all_runs.values())
89+
except Exception as e:
90+
print(f"Warning: Could not get branch workflows: {e}")
91+
6492
# Group by workflow name
6593
workflows = {}
6694
for run in runs:
@@ -88,6 +116,117 @@ def get_workflow_status(self, sha: str) -> Dict:
88116
"workflows": list(workflows.values()),
89117
}
90118

119+
def are_all_checks_passed(self, sha: str, branch: str = None) -> Tuple[bool, Dict]:
120+
"""
121+
Check if all checks and workflows have passed for a commit and branch.
122+
123+
This method checks:
124+
1. All checks for the HEAD SHA
125+
2. All workflow runs for the HEAD SHA
126+
3. All workflow runs for the branch (if provided)
127+
128+
Args:
129+
sha: Commit SHA
130+
branch: Optional branch name to also check workflows for the branch
131+
132+
Returns:
133+
Tuple of (all_passed: bool, details: Dict)
134+
"""
135+
import requests
136+
137+
# Get checks for the SHA
138+
try:
139+
checks_url = f"https://api.github.com/repos/{self.repo.full_name}/commits/{sha}/check-runs"
140+
headers = {"Accept": "application/vnd.github.v3+json"}
141+
auth = self.github._Github__requester._Requester__authorizationHeader
142+
if auth:
143+
headers["Authorization"] = auth
144+
145+
response = requests.get(checks_url, headers=headers, params={"per_page": 100})
146+
checks_data = response.json() if response.status_code == 200 else {}
147+
check_runs = checks_data.get("check_runs", [])
148+
except Exception as e:
149+
print(f"Warning: Could not get checks: {e}")
150+
check_runs = []
151+
152+
# Get status for the SHA
153+
try:
154+
status_url = f"https://api.github.com/repos/{self.repo.full_name}/commits/{sha}/status"
155+
response = requests.get(status_url, headers=headers)
156+
status_data = response.json() if response.status_code == 200 else {}
157+
combined_status = status_data.get("state", "unknown")
158+
except Exception as e:
159+
print(f"Warning: Could not get status: {e}")
160+
combined_status = "unknown"
161+
162+
# Get workflow status (includes branch workflows if branch is provided)
163+
workflow_status = self.get_workflow_status(sha, branch)
164+
165+
# Check if all checks passed
166+
all_checks_passed = (
167+
len(check_runs) > 0 and
168+
all(check["status"] == "completed" and check["conclusion"] == "success"
169+
for check in check_runs if check.get("conclusion") != "skipped") and
170+
combined_status == "success"
171+
)
172+
173+
# Check if all workflows passed
174+
workflows = workflow_status.get("workflows", [])
175+
all_workflows_passed = (
176+
len(workflows) > 0 and
177+
all(w.get("status") == "completed" and w.get("conclusion") == "success"
178+
for w in workflows)
179+
)
180+
181+
# Check for running workflows
182+
running_workflows = [
183+
w for w in workflows
184+
if w.get("status") in ["in_progress", "queued", "waiting"]
185+
]
186+
187+
# Check for failed workflows
188+
failed_workflows = [
189+
w for w in workflows
190+
if w.get("status") == "completed" and w.get("conclusion") == "failure"
191+
]
192+
193+
# Check for pending checks
194+
pending_checks = [
195+
check for check in check_runs
196+
if check.get("status") != "completed"
197+
]
198+
199+
# Check for failed checks
200+
failed_checks = [
201+
check for check in check_runs
202+
if check.get("status") == "completed" and
203+
check.get("conclusion") not in ["success", "skipped"]
204+
]
205+
206+
truly_all_passed = (
207+
all_checks_passed and
208+
all_workflows_passed and
209+
len(running_workflows) == 0 and
210+
len(failed_workflows) == 0 and
211+
len(pending_checks) == 0 and
212+
len(failed_checks) == 0
213+
)
214+
215+
details = {
216+
"all_checks_passed": all_checks_passed,
217+
"all_workflows_passed": all_workflows_passed,
218+
"truly_all_passed": truly_all_passed,
219+
"running_workflows": running_workflows,
220+
"failed_workflows": failed_workflows,
221+
"pending_checks": pending_checks,
222+
"failed_checks": failed_checks,
223+
"total_checks": len(check_runs),
224+
"total_workflows": len(workflows),
225+
"combined_status": combined_status,
226+
}
227+
228+
return (truly_all_passed, details)
229+
91230
def retry_workflow(self, sha: str, workflow_name: str) -> bool:
92231
"""
93232
Retry a specific workflow.

0 commit comments

Comments
 (0)