diff --git a/github_integration.py b/github_integration.py index 402056c..a34d677 100644 --- a/github_integration.py +++ b/github_integration.py @@ -5,6 +5,9 @@ import httpx import json +import io +import re +import zipfile from typing import Optional, Dict, Any, List from datetime import datetime, timedelta, timezone import asyncio @@ -268,6 +271,71 @@ async def get_workflow_run_status( print(f"Failed to get workflow run status: {e}") return {"status": "error", "conclusion": "failure", "error": str(e)} + async def get_workflow_run_logs_zip( + self, run_id: str, repo: str = GITHUB_REPO + ) -> Optional[bytes]: + """ + Download the workflow run logs as a zip archive. + """ + url = f"{self.base_url}/repos/{repo}/actions/runs/{run_id}/logs" + + try: + async with httpx.AsyncClient(follow_redirects=True) as client: + response = await client.get(url, headers=self.headers) + response.raise_for_status() + return response.content + except Exception as e: + print(f"Failed to download workflow run logs: {e}") + return None + + def parse_pr_status_records(self, log_zip_bytes: bytes) -> List[Dict[str, Any]]: + """ + Extract PR_STATUS JSON records from a workflow log zip archive. + """ + if not log_zip_bytes: + return [] + + records = [] + pattern = re.compile(r"PR_STATUS\s+(\{.*?\})") + + try: + with zipfile.ZipFile(io.BytesIO(log_zip_bytes)) as zip_file: + for info in zip_file.infolist(): + if info.is_dir(): + continue + with zip_file.open(info) as log_file: + for raw_line in log_file: + line = raw_line.decode("utf-8", errors="ignore") + for match in pattern.finditer(line): + try: + records.append(json.loads(match.group(1))) + except json.JSONDecodeError: + continue + except Exception as e: + print(f"Failed to parse workflow run logs: {e}") + + return records + + async def get_applied_prs_from_logs( + self, run_id: str, repo: str = GITHUB_REPO + ) -> List[Dict[str, Any]]: + """ + Return PR_STATUS records with state=applied from workflow run logs. + """ + log_zip_bytes = await self.get_workflow_run_logs_zip(run_id, repo) + records = self.parse_pr_status_records(log_zip_bytes) + + applied = {} + for record in records: + if record.get("state") != "applied": + continue + pr = record.get("pr") + if pr is None: + continue + applied[int(pr)] = record + + return [applied[pr] for pr in sorted(applied.keys())] + async def wait_for_workflow_completion( self, run_id: str, diff --git a/main.py b/main.py index 4547521..9bd7a6d 100644 --- a/main.py +++ b/main.py @@ -968,6 +968,63 @@ async def get_staging_nodes( } +@app.get("/api/staging/{run_id}/tested-prs") +async def get_staging_tested_prs( + run_id: int, + current_user: User = Depends(get_current_user_optional), + db: Session = Depends(get_db), +): + """Extract applied PRs from GitHub Actions logs for a staging run""" + if not current_user: + raise HTTPException(status_code=401, detail="Authentication required") + + staging_run = db.query(StagingRun).filter(StagingRun.id == run_id).first() + if not staging_run: + raise HTTPException(status_code=404, detail="Staging run not found") + + github_actions_id = None + for step in staging_run.steps: + if step.step_type == StagingStepType.GITHUB_WORKFLOW and step.github_actions_id: + github_actions_id = step.github_actions_id + break + + if not github_actions_id: + return { + "success": False, + "error": "GitHub Actions run ID not available for this staging run", + "applied_prs": [], + } + + github_token = get_setting(GITHUB_TOKEN) + if not github_token: + return { + "success": False, + "error": "GitHub token not configured", + "applied_prs": [], + } + + try: + from github_integration import GitHubWorkflowManager + + github_manager = GitHubWorkflowManager(github_token) + applied_prs = await github_manager.get_applied_prs_from_logs( + str(github_actions_id) + ) + + return { + "success": True, + "github_actions_id": str(github_actions_id), + "applied_prs": applied_prs, + } + except Exception as e: + print(f"Error extracting tested PRs: {e}") + return { + "success": False, + "error": f"Failed to extract tested PRs: {str(e)}", + "applied_prs": [], + } + + @app.post("/staging/{run_id}/cancel") async def cancel_staging_run( run_id: int, diff --git a/templates/staging_detail.html b/templates/staging_detail.html index 4b2b20b..c34c27b 100644 --- a/templates/staging_detail.html +++ b/templates/staging_detail.html @@ -201,6 +201,47 @@
{{ step.step_type.value.replace('_', ' ').title() }}
{% endfor %} + +
+
+ +
+
+ + + + + + + +
+
+
+
+
@@ -438,6 +479,118 @@
Cancel Run
{% endif %} -{% endblock %} \ No newline at end of file +{% endblock %}