2525from urllib .request import urlopen
2626
2727from github import Auth , Github # type: ignore[import-untyped]
28+ from jinja2 import Template
2829
2930from scripts .tooling .lib .known_good import load_known_good
3031
32+ # Jinja2 templates for markdown generation
33+ MODULE_STATUS_TEMPLATE = Template (
34+ """{% if status == 'disapproved' -%}
35+ - 🚫 **{{ module_name }}**: Changes requested by {{ disapproved_usernames|join(', ') }}
36+ {%- if approved_usernames %} (approved by {{ approved_usernames|join(', ') }}){% endif %}
37+ {% elif status == 'approved' -%}
38+ - ✅ **{{ module_name }}**: Approved by {{ approved_usernames|join(', ') }}
39+ {% else -%}
40+ - ❌ **{{ module_name }}**: Awaiting approval (requires one of: {{ required_approvers|join(', ') }})
41+ {% endif -%}
42+ """
43+ )
44+
45+ SUMMARY_TEMPLATE = Template (
46+ """### Release Approval Check Results
47+
48+ **Target Branch:** {{ base_branch }}
49+
50+ {% if all_approved -%}
51+ ✅ **Status:** All modules have required approvals
52+ {% else -%}
53+ ❌ **Status:** Some modules are missing required approvals
54+ {% endif %}
55+ #### Modules:
56+ {% for module in modules -%}
57+ {{ module }}
58+ {% endfor -%}
59+ """
60+ )
61+
3162
3263@dataclass
3364class ModuleResult :
@@ -264,6 +295,106 @@ def check_pr_reviews(
264295 }
265296
266297
298+ def _format_module_status (module_name : str , result : dict [str , Any ]) -> str :
299+ """Format a single module's approval status as markdown.
300+
301+ Args:
302+ module_name: Name of the module
303+ result: Module result dictionary containing status and approver information
304+
305+ Returns:
306+ Markdown-formatted status line
307+ """
308+ required_approvers = [m ["github" ] if isinstance (m , dict ) else str (m ) for m in result ["maintainers" ]]
309+
310+ return MODULE_STATUS_TEMPLATE .render (
311+ module_name = module_name ,
312+ status = result ["status" ],
313+ approved_usernames = result ["approvedUsernames" ],
314+ disapproved_usernames = result ["disapprovedUsernames" ],
315+ required_approvers = required_approvers ,
316+ )
317+
318+
319+ def _write_github_actions_summary (summary : str ) -> None :
320+ """Write summary to GitHub Actions step summary if available.
321+
322+ Args:
323+ summary: Markdown-formatted summary text
324+ """
325+ if "GITHUB_STEP_SUMMARY" in os .environ :
326+ with open (os .environ ["GITHUB_STEP_SUMMARY" ], "a" ) as f :
327+ f .write (summary )
328+ f .write ("\n " )
329+
330+
331+ def _post_pr_comment (
332+ summary : str ,
333+ repo_owner : str ,
334+ repo_name : str ,
335+ pr_number : int ,
336+ github_token : str ,
337+ ) -> None :
338+ """Post or update PR comment with approval summary.
339+
340+ Args:
341+ summary: Markdown-formatted summary text
342+ repo_owner: Repository owner
343+ repo_name: Repository name
344+ pr_number: Pull request number
345+ github_token: GitHub authentication token
346+ """
347+ try :
348+ auth = Auth .Token (github_token )
349+ github = Github (auth = auth )
350+ repo = github .get_repo (f"{ repo_owner } /{ repo_name } " )
351+ pr = repo .get_pull (pr_number )
352+
353+ comment_marker = "<!-- release-approval-check -->"
354+ full_comment = f"{ comment_marker } \n { summary } "
355+
356+ # Find existing comment from this workflow
357+ existing_comment = None
358+ for comment in pr .get_issue_comments ():
359+ if comment_marker in comment .body :
360+ existing_comment = comment
361+ break
362+
363+ # Update existing or create new comment
364+ if existing_comment :
365+ existing_comment .edit (full_comment )
366+ print ("Updated existing PR comment" , file = sys .stderr )
367+ else :
368+ pr .create_issue_comment (full_comment )
369+ print ("Created new PR comment" , file = sys .stderr )
370+ except Exception as e :
371+ print (f"Warning: Failed to post PR comment: { e } " , file = sys .stderr )
372+
373+
374+ def _build_summary_markdown (
375+ module_results : dict [str , dict [str , Any ]],
376+ all_approved : bool ,
377+ base_branch : str ,
378+ ) -> str :
379+ """Build the markdown summary text.
380+
381+ Args:
382+ module_results: Dictionary of module approval results
383+ all_approved: Whether all modules are approved
384+ base_branch: Target branch of the PR
385+
386+ Returns:
387+ Markdown-formatted summary string
388+ """
389+ modules = [_format_module_status (module_name , result ) for module_name , result in module_results .items ()]
390+
391+ return SUMMARY_TEMPLATE .render (
392+ base_branch = base_branch ,
393+ all_approved = all_approved ,
394+ modules = modules ,
395+ )
396+
397+
267398def generate_summary (
268399 module_results : dict [str , dict [str , Any ]],
269400 all_approved : bool ,
@@ -287,65 +418,12 @@ def generate_summary(
287418 Returns:
288419 Markdown-formatted summary string
289420 """
290- summary = "### Release Approval Check Results\n \n "
291- summary += f"**Target Branch:** { base_branch } \n \n "
421+ summary = _build_summary_markdown (module_results , all_approved , base_branch )
292422
293- if all_approved :
294- summary += "✅ **Status:** All modules have required approvals\n \n "
295- else :
296- summary += "❌ **Status:** Some modules are missing required approvals\n \n "
297-
298- summary += "#### Modules:\n "
299-
300- for module_name , result in module_results .items ():
301- if result ["status" ] == "disapproved" :
302- disapprovers = ", " .join (result ["disapprovedUsernames" ])
303- approvers_text = ""
304- if result ["approvedUsernames" ]:
305- approvers_text = f" (approved by { ', ' .join (result ['approvedUsernames' ])} )"
306- summary += f"- 🚫 **{ module_name } **: Changes requested by { disapprovers } { approvers_text } \n "
307- elif result ["status" ] == "approved" :
308- approvers = ", " .join (result ["approvedUsernames" ])
309- summary += f"- ✅ **{ module_name } **: Approved by { approvers } \n "
310- else : # pending
311- required_approvers = ", " .join (
312- [m ["github" ] if isinstance (m , dict ) else str (m ) for m in result ["maintainers" ]]
313- )
314- summary += f"- ❌ **{ module_name } **: Awaiting approval (requires one of: { required_approvers } )\n "
315-
316- # Write to GitHub Actions step summary if available
317- if "GITHUB_STEP_SUMMARY" in os .environ :
318- with open (os .environ ["GITHUB_STEP_SUMMARY" ], "a" ) as f :
319- f .write (summary )
320- f .write ("\n " )
423+ _write_github_actions_summary (summary )
321424
322- # Post as PR comment if credentials provided
323425 if all ([repo_owner , repo_name , pr_number , github_token ]):
324- try :
325- auth = Auth .Token (github_token )
326- github = Github (auth = auth )
327- repo = github .get_repo (f"{ repo_owner } /{ repo_name } " )
328- pr = repo .get_pull (pr_number )
329-
330- # Check if there's already a comment from this workflow
331- comment_marker = "<!-- release-approval-check -->"
332- existing_comment = None
333-
334- for comment in pr .get_issue_comments ():
335- if comment_marker in comment .body :
336- existing_comment = comment
337- break
338-
339- full_comment = f"{ comment_marker } \n { summary } "
340-
341- if existing_comment :
342- existing_comment .edit (full_comment )
343- print ("Updated existing PR comment" , file = sys .stderr )
344- else :
345- pr .create_issue_comment (full_comment )
346- print ("Created new PR comment" , file = sys .stderr )
347- except Exception as e :
348- print (f"Warning: Failed to post PR comment: { e } " , file = sys .stderr )
426+ _post_pr_comment (summary , repo_owner , repo_name , pr_number , github_token )
349427
350428 return summary
351429
0 commit comments