1111from datetime import datetime , timezone
1212from pathlib import Path
1313from urllib .error import HTTPError
14+ from urllib .parse import quote
1415from urllib .request import Request , urlopen
1516
1617
@@ -1263,13 +1264,14 @@ def publish_result_if_configured(task, result_path, token):
12631264
12641265def publication_comment_body (task , result ):
12651266 status = result .get ("status" ) or "unknown"
1266- summary = result .get ("summary" ) or "No summary returned."
1267- pr_body = result .get ("pr_body" ) or ""
12681267 files_changed = result .get ("files_changed" ) or []
12691268 commits = result .get ("commits" ) or []
12701269 task_id = task .get ("task_id" ) or ""
12711270 evidence = task .get ("review_evidence" ) or {}
12721271 review = result .get ("review" ) or {}
1272+ known_files = github_known_file_set (task , review )
1273+ summary = link_github_file_mentions (task , result .get ("summary" ) or "No summary returned." , known_files )
1274+ pr_body = link_github_file_mentions (task , result .get ("pr_body" ) or "" , known_files )
12731275
12741276 parts = [
12751277 "## Cody dogfood result" ,
@@ -1295,10 +1297,14 @@ def publication_comment_body(task, result):
12951297 ]
12961298 )
12971299 if changed_files :
1298- parts .append ("- Files: {}" .format (", " .join ("`{}`" .format (f ) for f in changed_files [:20 ])))
1300+ parts .append (
1301+ "- Files: {}" .format (
1302+ ", " .join (github_file_markdown (task , f ) for f in changed_files [:20 ])
1303+ )
1304+ )
12991305 else :
13001306 parts .append ("- No PR review evidence was captured for this run." )
1301- parts .extend (structured_review_lines (review ))
1307+ parts .extend (structured_review_lines (review , task , known_files ))
13021308 parts .extend (
13031309 [
13041310 "" ,
@@ -1311,6 +1317,179 @@ def publication_comment_body(task, result):
13111317 return "\n " .join (parts )
13121318
13131319
1320+ def github_blob_base (task ):
1321+ repository = str (task .get ("repository" ) or "" ).strip ()
1322+ if not re .match (r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$" , repository ):
1323+ return None
1324+ evidence = task .get ("review_evidence" ) or {}
1325+ ref = str (
1326+ evidence .get ("head_sha" )
1327+ or evidence .get ("workspace_head_sha" )
1328+ or task .get ("default_branch" )
1329+ or ""
1330+ ).strip ()
1331+ if not ref :
1332+ return None
1333+ return "https://github.com/{}/blob/{}" .format (repository , quote (ref , safe = "/" ))
1334+
1335+
1336+ def parse_repo_relative_path (raw_path ):
1337+ raw = str (raw_path )
1338+ display = raw .strip ()
1339+ if not display or display != raw or re .search (r"[\s<>\[\]]" , display ):
1340+ return None
1341+ if display .startswith (("/" , "\\ " )) or re .match (r"^[A-Za-z]:[\\/]" , display ):
1342+ return None
1343+
1344+ path = display .replace ("\\ " , "/" )
1345+ line = None
1346+ end_line = None
1347+ line_match = re .match (r"^(.*):(\d+)(?:-(\d+))?$" , path )
1348+ if line_match :
1349+ path = line_match .group (1 )
1350+ line = int (line_match .group (2 ))
1351+ end_line = int (line_match .group (3 )) if line_match .group (3 ) else None
1352+ if re .match (r"^[A-Za-z][A-Za-z0-9+.-]*:" , path ):
1353+ return None
1354+ while path .startswith ("./" ):
1355+ path = path [2 :]
1356+
1357+ segments = path .split ("/" )
1358+ filename = segments [- 1 ] if segments else ""
1359+ if (
1360+ not path
1361+ or "//" in path
1362+ or any (not segment or segment in {"." , ".." } for segment in segments )
1363+ or not re .search (r"\.[A-Za-z][A-Za-z0-9._-]{0,15}$" , filename )
1364+ ):
1365+ return None
1366+ return {"display" : display , "path" : path , "line" : line , "end_line" : end_line }
1367+
1368+
1369+ def github_file_markdown (task , raw_path , line_override = None ):
1370+ target = parse_repo_relative_path (raw_path )
1371+ base = github_blob_base (task )
1372+ if not target or not base :
1373+ return "`{}`" .format (raw_path )
1374+ encoded_path = "/" .join (quote (segment , safe = "" ) for segment in target ["path" ].split ("/" ))
1375+ line = line_override if line_override is not None else target ["line" ]
1376+ end_line = None if line_override is not None else target ["end_line" ]
1377+ if isinstance (line , int ) and line > 0 :
1378+ line_anchor = "#L{}" .format (line )
1379+ if isinstance (end_line , int ) and end_line > line :
1380+ line_anchor = "{}-L{}" .format (line_anchor , end_line )
1381+ else :
1382+ line_anchor = ""
1383+ return "[`{}`]({}/{}{})" .format (target ["display" ], base , encoded_path , line_anchor )
1384+
1385+
1386+ def github_known_file_set (task , review = None ):
1387+ known_files = set ()
1388+ evidence = task .get ("review_evidence" ) or {}
1389+ review = review or {}
1390+
1391+ def add_files (value ):
1392+ if not isinstance (value , list ):
1393+ return
1394+ for item in value :
1395+ target = parse_repo_relative_path (str (item ))
1396+ if target :
1397+ known_files .add (target ["path" ])
1398+
1399+ add_files (evidence .get ("changed_files" ))
1400+ add_files (review .get ("reviewed_files" ))
1401+ add_files (review .get ("supporting_files" ))
1402+ return known_files
1403+
1404+
1405+ def inline_code_with_github_file_links (task , raw_text ):
1406+ raw_text = str (raw_text )
1407+ direct = github_file_markdown (task , raw_text )
1408+ if direct != "`{}`" .format (raw_text ):
1409+ return direct
1410+
1411+ linked_any = False
1412+ parts = []
1413+ for part in re .split (r"(\s+)" , raw_text ):
1414+ if not part or part .isspace ():
1415+ parts .append (part )
1416+ continue
1417+ linked = github_file_markdown (task , part )
1418+ if linked != "`{}`" .format (part ):
1419+ linked_any = True
1420+ parts .append (linked )
1421+ else :
1422+ parts .append ("`{}`" .format (part ))
1423+
1424+ return "" .join (parts ) if linked_any else "`{}`" .format (raw_text )
1425+
1426+
1427+ def bare_file_mention_allowed (task , raw_path , known_files ):
1428+ target = parse_repo_relative_path (raw_path )
1429+ if not target :
1430+ return False
1431+ return "/" in target ["path" ] or target ["path" ] in known_files
1432+
1433+
1434+ def link_bare_github_file_mentions (task , text , known_files ):
1435+ markdown_link_pattern = re .compile (r"(\[[^\]]+\]\([^)]+\))" )
1436+ bare_path_pattern = re .compile (
1437+ r"(^|[^\w./:\[\]()`-])"
1438+ r"([A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)*\.[A-Za-z][A-Za-z0-9_-]{0,15}(?::\d+(?:-\d+)?)?)"
1439+ r"(?=$|[^\w/-])"
1440+ )
1441+
1442+ def replace_segment (segment ):
1443+ def replace_match (match ):
1444+ prefix = match .group (1 )
1445+ raw_path = match .group (2 )
1446+ if not bare_file_mention_allowed (task , raw_path , known_files ):
1447+ return match .group (0 )
1448+ linked = github_file_markdown (task , raw_path )
1449+ return match .group (0 ) if linked == "`{}`" .format (raw_path ) else "{}{}" .format (prefix , linked )
1450+
1451+ return bare_path_pattern .sub (replace_match , segment )
1452+
1453+ return "" .join (
1454+ segment if index % 2 == 1 else replace_segment (segment )
1455+ for index , segment in enumerate (markdown_link_pattern .split (str (text )))
1456+ )
1457+
1458+
1459+ def link_github_file_mentions (task , text , known_files = None ):
1460+ known_files = github_known_file_set (task ) if known_files is None else known_files
1461+ fence_pattern = re .compile (r"(```[\s\S]*?```)" )
1462+ segments = fence_pattern .split (str (text ))
1463+
1464+ def replace_segment (segment ):
1465+ def replace_match (match ):
1466+ raw_path = match .group (1 )
1467+ already_link_text = (
1468+ match .start () > 0
1469+ and segment [match .start () - 1 ] == "["
1470+ and segment [match .end () : match .end () + 2 ] == "]("
1471+ )
1472+ if already_link_text :
1473+ return match .group (0 )
1474+ linked = inline_code_with_github_file_links (task , raw_path )
1475+ return match .group (0 ) if linked == "`{}`" .format (raw_path ) else linked
1476+
1477+ linked_inline_code = re .sub (r"`([^`\n]+)`" , replace_match , segment )
1478+ return link_bare_github_file_mentions (task , linked_inline_code , known_files )
1479+
1480+ return "" .join (
1481+ segment if index % 2 == 1 else replace_segment (segment )
1482+ for index , segment in enumerate (segments )
1483+ )
1484+
1485+
1486+ def review_test_command_markdown (task , raw_command ):
1487+ command = str (raw_command ).strip ()
1488+ if not command :
1489+ return "`unknown command`"
1490+ return inline_code_with_github_file_links (task , command )
1491+
1492+
13141493def review_fix_loop_lines (task ):
13151494 loops = task .get ("review_fix_loops" ) or []
13161495 if not loops :
@@ -1335,7 +1514,8 @@ def review_fix_loop_lines(task):
13351514 return lines
13361515
13371516
1338- def structured_review_lines (review ):
1517+ def structured_review_lines (review , task , known_files = None ):
1518+ known_files = github_known_file_set (task , review ) if known_files is None else known_files
13391519 if not review :
13401520 return ["" , "### Structured review" , "- No structured review result was emitted." ]
13411521
@@ -1351,7 +1531,7 @@ def structured_review_lines(review):
13511531 if reviewed_files :
13521532 lines .append (
13531533 "- Reviewed file list: {}" .format (
1354- ", " .join ("`{}`" . format ( path ) for path in reviewed_files [:20 ])
1534+ ", " .join (github_file_markdown ( task , path ) for path in reviewed_files [:20 ])
13551535 )
13561536 )
13571537 if len (reviewed_files ) > 20 :
@@ -1362,7 +1542,7 @@ def structured_review_lines(review):
13621542 if supporting_files :
13631543 lines .append (
13641544 "- Supporting file list: {}" .format (
1365- ", " .join ("`{}`" . format ( path ) for path in supporting_files [:20 ])
1545+ ", " .join (github_file_markdown ( task , path ) for path in supporting_files [:20 ])
13661546 )
13671547 )
13681548 if len (supporting_files ) > 20 :
@@ -1374,11 +1554,12 @@ def structured_review_lines(review):
13741554 location = finding .get ("file" ) or "unknown file"
13751555 if finding .get ("line" ) is not None :
13761556 location = "{}:{}" .format (location , finding .get ("line" ))
1557+ linked_location = github_file_markdown (task , location , finding .get ("line" ))
13771558 lines .append (
13781559 " {}. `{}` {} - {}" .format (
13791560 index ,
13801561 finding .get ("severity" ) or "unknown" ,
1381- location ,
1562+ linked_location ,
13821563 finding .get ("title" ) or "Untitled finding" ,
13831564 )
13841565 )
@@ -1387,16 +1568,17 @@ def structured_review_lines(review):
13871568
13881569 no_findings_reason = review .get ("no_findings_reason" )
13891570 if no_findings_reason :
1390- lines .append ("- No-findings reason: {}" .format (no_findings_reason ))
1571+ lines .append ("- No-findings reason: {}" .format (link_github_file_mentions ( task , no_findings_reason , known_files ) ))
13911572
13921573 tests_run = review .get ("tests_run" ) or []
13931574 lines .append ("- Tests reported by runtime: {}" .format (len (tests_run )))
13941575 for test in tests_run [:10 ]:
13951576 summary = test .get ("output_summary" )
1396- suffix = " - {}" .format (summary ) if summary else ""
1577+ command = review_test_command_markdown (task , test .get ("command" ) or "unknown command" )
1578+ suffix = " - {}" .format (link_github_file_mentions (task , summary , known_files )) if summary else ""
13971579 lines .append (
1398- " - `{}` : `{}`{}" .format (
1399- test . get ( " command" ) or "unknown command" ,
1580+ " - {} : `{}`{}" .format (
1581+ command ,
14001582 test .get ("status" ) or "unknown" ,
14011583 suffix ,
14021584 )
0 commit comments