11# noqa: D100
22import re
3+ import textwrap
4+
35from collections .abc import AsyncGenerator
46
57import aiohttp
@@ -45,7 +47,9 @@ async def Query(self, repo: Repository) -> AsyncGenerator[ResultInfo | ErrorInfo
4547
4648 github_url = repo .remote_url .removesuffix (".git" )
4749
48- async for info in self ._GenerateStandardInfo (repo , github_url ):
50+ persisted_info : dict [str , object ] = {}
51+
52+ async for info in self ._GenerateStandardInfo (repo , github_url , persisted_info ):
4953 yield info
5054
5155 async for info in self ._GenerateIssueInfo (repo , github_url ):
@@ -57,13 +61,21 @@ async def Query(self, repo: Repository) -> AsyncGenerator[ResultInfo | ErrorInfo
5761 async for info in self ._GenerateSecurityAlertInfo (repo , github_url ):
5862 yield info
5963
64+ default_branch = persisted_info .get ("default_branch" )
65+
66+ # Only generate CI/CD info if we have a default branch (repo API succeeded)
67+ if isinstance (default_branch , str ):
68+ async for info in self ._GenerateCICDInfo (repo , github_url , default_branch ):
69+ yield info
70+
6071 # ----------------------------------------------------------------------
6172 # ----------------------------------------------------------------------
6273 # ----------------------------------------------------------------------
6374 async def _GenerateStandardInfo (
6475 self ,
6576 repo : Repository ,
6677 github_url : str ,
78+ persisted_info : dict [str , object ],
6779 ) -> AsyncGenerator [ResultInfo | ErrorInfo ]:
6880 try :
6981 async with self ._session .get (
@@ -72,6 +84,9 @@ async def _GenerateStandardInfo(
7284 response .raise_for_status ()
7385 result = await response .json ()
7486
87+ # Capture default_branch for use by CI/CD status
88+ persisted_info ["default_branch" ] = result .get ("default_branch" )
89+
7590 yield ResultInfo (
7691 repo ,
7792 (self .__class__ .__name__ , "stars" ),
@@ -135,7 +150,7 @@ async def _GenerateIssueInfo(
135150
136151 label_str = f" [{ ', ' .join (issue_labels )} ]" if issue_labels else ""
137152
138- issue_data .append (f" • #{ issue_number } { label_str } { issue_title } (by { issue_author } )" )
153+ issue_data .append (f"• #{ issue_number } { label_str } { issue_title } (by { issue_author } )" )
139154
140155 # Build additional info with issue details
141156 additional_info_lines = [
@@ -191,7 +206,7 @@ async def _GeneratePullRequestInfo(
191206
192207 draft_indicator = "[DRAFT] " if pr_draft else ""
193208
194- pr_data .append (f" • #{ pr_number } { draft_indicator } { pr_title } (by { pr_author } )" )
209+ pr_data .append (f"• #{ pr_number } { draft_indicator } { pr_title } (by { pr_author } )" )
195210
196211 additional_info_lines = [
197212 f"Pull Requests: { github_url } /pulls" ,
@@ -246,7 +261,7 @@ async def _GenerateSecurityAlertInfo(
246261 package = alert .get ("security_vulnerability" , {}).get ("package" , {})
247262
248263 alert_data .append (
249- f" • [{ advisory .get ('severity' , 'unknown' ).upper ()} ] { package .get ('name' , 'unknown' )} : { advisory .get ('summary' , 'No summary' )} "
264+ f"• [{ advisory .get ('severity' , 'unknown' ).upper ()} ] { package .get ('name' , 'unknown' )} : { advisory .get ('summary' , 'No summary' )} "
250265 )
251266
252267 # Build display value with icon based on severity
@@ -290,6 +305,118 @@ async def _GenerateSecurityAlertInfo(
290305 ex ,
291306 )
292307
308+ # ----------------------------------------------------------------------
309+ async def _GenerateCICDInfo (
310+ self ,
311+ repo : Repository ,
312+ github_url : str ,
313+ default_branch : str ,
314+ ) -> AsyncGenerator [ResultInfo | ErrorInfo ]:
315+ info_key = (self .__class__ .__name__ , "cicd_status" )
316+
317+ try :
318+ url = f"https://api.github.com/repos/{ repo .github_owner } /{ repo .github_repo } /actions/runs?branch={ default_branch } &per_page=100"
319+
320+ async with self ._session .get (url ) as response :
321+ response .raise_for_status ()
322+ result = await response .json ()
323+
324+ workflow_runs = result .get ("workflow_runs" , [])
325+
326+ if not workflow_runs :
327+ yield ResultInfo (
328+ repo ,
329+ info_key ,
330+ "🔘" ,
331+ textwrap .dedent (
332+ """\
333+ CI/CD Status: {github_url}/actions
334+
335+ No workflow runs found for branch '{default_branch}'
336+ """ ,
337+ ).format (
338+ github_url = github_url ,
339+ default_branch = default_branch ,
340+ ),
341+ )
342+ return
343+
344+ # Group by workflow name and take only the most recent run for each
345+ latest_per_workflow : dict [str , dict ] = {}
346+
347+ for run in workflow_runs :
348+ workflow_id = run ["workflow_id" ]
349+
350+ if workflow_id not in latest_per_workflow :
351+ latest_per_workflow [workflow_id ] = run
352+
353+ # Count statuses based on most recent run per workflow
354+ status_counts = {
355+ "success" : 0 ,
356+ "failure" : 0 ,
357+ "in_progress" : 0 ,
358+ }
359+
360+ run_details : list [str ] = []
361+
362+ for run in latest_per_workflow .values ():
363+ conclusion = run .get ("conclusion" )
364+ status = run .get ("status" )
365+
366+ # Determine the effective status
367+ if status in ("in_progress" , "queued" , "pending" , "waiting" ):
368+ status_counts ["in_progress" ] += 1
369+ status_label = "IN PROG"
370+ elif conclusion == "success" :
371+ status_counts ["success" ] += 1
372+ status_label = " PASS "
373+ elif conclusion in ("failure" , "cancelled" , "timed_out" ):
374+ status_counts ["failure" ] += 1
375+ status_label = " FAIL "
376+ else :
377+ status_label = conclusion or status or "UNKNOWN"
378+
379+ run_details .append (f"• [{ status_label } ] { run ['created_at' ]} { run ['path' ]} : { run ['name' ]} " )
380+
381+ # Determine display icon based on priority: failure > in_progress > success
382+ if status_counts ["failure" ] > 0 :
383+ display_icon = "❌"
384+ elif status_counts ["in_progress" ] > 0 :
385+ display_icon = "⏳"
386+ elif status_counts ["success" ] > 0 :
387+ display_icon = "✅"
388+ else :
389+ display_icon = "🔘"
390+
391+ # Build additional info
392+ additional_info_lines = [
393+ f"CI/CD Status: { github_url } /actions" ,
394+ "" ,
395+ f"Default Branch: { default_branch } " ,
396+ "" ,
397+ "Summary (latest run per workflow):" ,
398+ f" Successful: { status_counts ['success' ]} " ,
399+ f" Failed: { status_counts ['failure' ]} " ,
400+ f" In Progress: { status_counts ['in_progress' ]} " ,
401+ "" ,
402+ ]
403+
404+ additional_info_lines .extend (run_details )
405+
406+ yield ResultInfo (
407+ repo ,
408+ info_key ,
409+ display_icon ,
410+ "\n " .join (additional_info_lines ),
411+ )
412+
413+ except Exception as ex :
414+ yield ErrorInfo (
415+ repo ,
416+ info_key ,
417+ ex ,
418+ )
419+
293420 # ----------------------------------------------------------------------
294421 async def _GeneratePaginatedResults (self , raw_url : str ) -> AsyncGenerator [dict ]:
295422 url : str | None = f"{ raw_url } ?state=open&per_page=100"
0 commit comments