"""Summary Reporter module for the PAT Manager system. Generates a human-readable summary report of PAT lifecycle check results suitable for GitHub Actions log output. """ from __future__ import annotations from .models import CheckResult, PatStatus, RotationResult class SummaryReporter: """Produces a formatted summary report of PAT check and rotation results. The report includes a table of all checked PATs with their status and details, plus a rotation results section if any tokens were rotated. """ def report( self, results: list[CheckResult], rotations: list[RotationResult] ) -> str: """Generate a plain-text summary report. Args: results: List of check results for all PATs in the registry. rotations: List of rotation results for tokens that were rotated. Returns: A formatted plain-text string suitable for printing to the GitHub Actions log. """ lines: list[str] = [] lines.append("=== PAT Lifecycle Check Summary ===") lines.append("") # Build the table lines.append(self._build_table(results)) # Add rotation section if there are rotation results successful_rotations = [r for r in rotations if r.success] if successful_rotations: lines.append("") lines.append("--- Rotation Results ---") for rotation in successful_rotations: lines.append(self._format_rotation(rotation)) return "\n".join(lines) def _build_table(self, results: list[CheckResult]) -> str: """Build the formatted results table. Args: results: List of check results to display. Returns: Formatted table string with header, separator, and data rows. """ # Column headers header_service = "Service" header_token = "Token Name" header_status = "Status" header_details = "Details" # Calculate column widths based on content service_width = max( len(header_service), *(len(r.entry.service) for r in results) if results else [0], ) token_width = max( len(header_token), *(len(r.entry.token_name) for r in results) if results else [0], ) status_width = max( len(header_status), *(len(r.status.value) for r in results) if results else [0], ) details_width = max( len(header_details), *(len(self._get_details(r)) for r in results) if results else [0], ) # Format header header = ( f"{header_service:<{service_width}} | " f"{header_token:<{token_width}} | " f"{header_status:<{status_width}} | " f"{header_details:<{details_width}}" ) # Format separator separator = ( f"{'-' * service_width}-+-" f"{'-' * token_width}-+-" f"{'-' * status_width}-+-" f"{'-' * details_width}" ) # Format data rows rows: list[str] = [] for result in results: details = self._get_details(result) row = ( f"{result.entry.service:<{service_width}} | " f"{result.entry.token_name:<{token_width}} | " f"{result.status.value:<{status_width}} | " f"{details:<{details_width}}" ) rows.append(row) table_lines = [header, separator] + rows return "\n".join(table_lines) def _get_details(self, result: CheckResult) -> str: """Get the details string for a check result. Args: result: The check result to describe. Returns: A human-readable details string based on the PAT status. """ if result.status == PatStatus.CHECK_FAILED: return result.error_message or "Unknown error" if result.status == PatStatus.EXPIRED: return "0 days remaining" if result.days_remaining is not None: return f"{result.days_remaining} days remaining" return "" def _format_rotation(self, rotation: RotationResult) -> str: """Format a single rotation result line. Args: rotation: A successful rotation result. Returns: Formatted string like: "GitLab/token-name: Rotated successfully (old: 2025-07-01, new: 2025-08-01)" """ if rotation.entry is not None: service = rotation.entry.service token_name = rotation.entry.token_name else: service = "Unknown" token_name = "unknown-token" old_date = rotation.old_expiry_date or "N/A" new_date = rotation.new_expiry_date or "N/A" return ( f"{service}/{token_name}: " f"Rotated successfully (old: {old_date}, new: {new_date})" )