|
2 | 2 | """ |
3 | 3 | Workflow Manager - Handles GitHub Actions workflow operations. |
4 | 4 | """ |
5 | | -from typing import Dict, List, Optional |
| 5 | +from typing import Dict, List, Optional, Tuple |
6 | 6 | from github import Github |
7 | 7 | from github.GithubException import GithubException |
8 | 8 |
|
@@ -49,18 +49,46 @@ def get_workflow_runs(self, sha: str) -> List[Dict]: |
49 | 49 |
|
50 | 50 | return workflow_runs |
51 | 51 |
|
52 | | - def get_workflow_status(self, sha: str) -> Dict: |
| 52 | + def get_workflow_status(self, sha: str, branch: str = None) -> Dict: |
53 | 53 | """ |
54 | | - Get status summary of all workflows for a commit. |
| 54 | + Get status summary of all workflows for a commit and optionally branch. |
55 | 55 |
|
56 | 56 | Args: |
57 | 57 | sha: Commit SHA |
| 58 | + branch: Optional branch name to also check workflows for the branch |
58 | 59 |
|
59 | 60 | Returns: |
60 | 61 | Dictionary with workflow status information |
61 | 62 | """ |
| 63 | + # Get runs for the specific SHA |
62 | 64 | runs = self.get_workflow_runs(sha) |
63 | 65 |
|
| 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 | + |
64 | 92 | # Group by workflow name |
65 | 93 | workflows = {} |
66 | 94 | for run in runs: |
@@ -88,6 +116,117 @@ def get_workflow_status(self, sha: str) -> Dict: |
88 | 116 | "workflows": list(workflows.values()), |
89 | 117 | } |
90 | 118 |
|
| 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 | + |
91 | 230 | def retry_workflow(self, sha: str, workflow_name: str) -> bool: |
92 | 231 | """ |
93 | 232 | Retry a specific workflow. |
|
0 commit comments