#!/usr/bin/env python3 """Post or update a PR comment with memory impact analysis results. This script creates or updates a GitHub PR comment with memory usage changes. It uses the GitHub CLI (gh) to manage comments and maintains a single comment that gets updated on subsequent runs. """ from __future__ import annotations import argparse import json import subprocess import sys # Comment marker to identify our memory impact comments COMMENT_MARKER = "" def format_bytes(bytes_value: int) -> str: """Format bytes value with comma separators. Args: bytes_value: Number of bytes Returns: Formatted string with comma separators (e.g., "1,234 bytes") """ return f"{bytes_value:,} bytes" def format_change(before: int, after: int) -> str: """Format memory change with delta and percentage. Args: before: Memory usage before change (in bytes) after: Memory usage after change (in bytes) Returns: Formatted string with delta and percentage """ delta = after - before percentage = 0.0 if before == 0 else (delta / before) * 100 # Format delta with sign and always show in bytes for precision if delta > 0: delta_str = f"+{delta:,} bytes" emoji = "📈" elif delta < 0: delta_str = f"{delta:,} bytes" emoji = "📉" else: delta_str = "+0 bytes" emoji = "➡️" # Format percentage with sign if percentage > 0: pct_str = f"+{percentage:.2f}%" elif percentage < 0: pct_str = f"{percentage:.2f}%" else: pct_str = "0.00%" return f"{emoji} {delta_str} ({pct_str})" def create_comment_body( component: str, platform: str, target_ram: int, target_flash: int, pr_ram: int, pr_flash: int, ) -> str: """Create the comment body with memory impact analysis. Args: component: Component name platform: Platform name target_ram: RAM usage in target branch target_flash: Flash usage in target branch pr_ram: RAM usage in PR branch pr_flash: Flash usage in PR branch Returns: Formatted comment body """ ram_change = format_change(target_ram, pr_ram) flash_change = format_change(target_flash, pr_flash) return f"""{COMMENT_MARKER} ## Memory Impact Analysis **Component:** `{component}` **Platform:** `{platform}` | Metric | Target Branch | This PR | Change | |--------|--------------|---------|--------| | **RAM** | {format_bytes(target_ram)} | {format_bytes(pr_ram)} | {ram_change} | | **Flash** | {format_bytes(target_flash)} | {format_bytes(pr_flash)} | {flash_change} | --- *This analysis runs automatically when a single component changes. Memory usage is measured from a representative test configuration.* """ def find_existing_comment(pr_number: str) -> str | None: """Find existing memory impact comment on the PR. Args: pr_number: PR number Returns: Comment numeric ID if found, None otherwise """ try: print( f"DEBUG: Looking for existing comment on PR #{pr_number}", file=sys.stderr ) # Use gh api to get comments directly - this returns the numeric id field result = subprocess.run( [ "gh", "api", f"/repos/{{owner}}/{{repo}}/issues/{pr_number}/comments", "--jq", ".[] | {id, body}", ], capture_output=True, text=True, check=True, ) print( f"DEBUG: gh api comments output (first 500 chars):\n{result.stdout[:500]}", file=sys.stderr, ) # Parse comments and look for our marker comment_count = 0 for line in result.stdout.strip().split("\n"): if not line: continue try: comment = json.loads(line) comment_count += 1 comment_id = comment.get("id") print( f"DEBUG: Checking comment {comment_count}: id={comment_id}", file=sys.stderr, ) body = comment.get("body", "") if COMMENT_MARKER in body: print( f"DEBUG: Found existing comment with id={comment_id}", file=sys.stderr, ) # Return the numeric id return str(comment_id) print("DEBUG: Comment does not contain marker", file=sys.stderr) except json.JSONDecodeError as e: print(f"DEBUG: JSON decode error: {e}", file=sys.stderr) continue print( f"DEBUG: No existing comment found (checked {comment_count} comments)", file=sys.stderr, ) return None except subprocess.CalledProcessError as e: print(f"Error finding existing comment: {e}", file=sys.stderr) if e.stderr: print(f"stderr: {e.stderr.decode()}", file=sys.stderr) return None def post_or_update_comment(pr_number: str, comment_body: str) -> bool: """Post a new comment or update existing one. Args: pr_number: PR number comment_body: Comment body text Returns: True if successful, False otherwise """ # Look for existing comment existing_comment_id = find_existing_comment(pr_number) try: if existing_comment_id and existing_comment_id != "None": # Update existing comment print( f"DEBUG: Updating existing comment {existing_comment_id}", file=sys.stderr, ) result = subprocess.run( [ "gh", "api", f"/repos/{{owner}}/{{repo}}/issues/comments/{existing_comment_id}", "-X", "PATCH", "-f", f"body={comment_body}", ], check=True, capture_output=True, text=True, ) print(f"DEBUG: Update response: {result.stdout}", file=sys.stderr) else: # Post new comment print( f"DEBUG: Posting new comment (existing_comment_id={existing_comment_id})", file=sys.stderr, ) result = subprocess.run( ["gh", "pr", "comment", pr_number, "--body", comment_body], check=True, capture_output=True, text=True, ) print(f"DEBUG: Post response: {result.stdout}", file=sys.stderr) print("Comment posted/updated successfully", file=sys.stderr) return True except subprocess.CalledProcessError as e: print(f"Error posting/updating comment: {e}", file=sys.stderr) if e.stderr: print( f"stderr: {e.stderr.decode() if isinstance(e.stderr, bytes) else e.stderr}", file=sys.stderr, ) if e.stdout: print( f"stdout: {e.stdout.decode() if isinstance(e.stdout, bytes) else e.stdout}", file=sys.stderr, ) return False def main() -> int: """Main entry point.""" parser = argparse.ArgumentParser( description="Post or update PR comment with memory impact analysis" ) parser.add_argument("--pr-number", required=True, help="PR number") parser.add_argument("--component", required=True, help="Component name") parser.add_argument("--platform", required=True, help="Platform name") parser.add_argument( "--target-ram", type=int, required=True, help="Target branch RAM usage" ) parser.add_argument( "--target-flash", type=int, required=True, help="Target branch flash usage" ) parser.add_argument("--pr-ram", type=int, required=True, help="PR branch RAM usage") parser.add_argument( "--pr-flash", type=int, required=True, help="PR branch flash usage" ) args = parser.parse_args() # Create comment body comment_body = create_comment_body( component=args.component, platform=args.platform, target_ram=args.target_ram, target_flash=args.target_flash, pr_ram=args.pr_ram, pr_flash=args.pr_flash, ) # Post or update comment success = post_or_update_comment(args.pr_number, comment_body) return 0 if success else 1 if __name__ == "__main__": sys.exit(main())