diff --git a/.github/scripts/README.md b/.github/scripts/README.md new file mode 100644 index 0000000000..74d50d7ddf --- /dev/null +++ b/.github/scripts/README.md @@ -0,0 +1,44 @@ +# GitHub Workflow Scripts + +This directory contains scripts used by GitHub Actions workflows. + +## xaml-preview.py + +A Python script that processes XAML files and generates formatted previews for pull request comments. + +### Features + +- **XAML Analysis**: Parses XAML files to extract root elements, controls, and metadata +- **Formatted Output**: Creates markdown-formatted previews with syntax highlighting +- **File Information**: Shows file size, line count, and used UI controls +- **Change Detection**: Handles added, modified, and removed files differently +- **Error Handling**: Gracefully handles malformed XAML or missing files + +### Usage + +```bash +python3 xaml-preview.py --modified "file1.xaml file2.xaml" --added "file3.xaml" --removed "file4.xaml" +``` + +### Integration + +This script is used by the `.github/workflows/xaml-preview.yml` workflow, which automatically: + +1. Triggers on pull requests that modify `.xaml` files +2. Analyzes the changed XAML files +3. Posts formatted previews as PR comments +4. Updates existing comments when new changes are pushed + +### Output Format + +The script generates a markdown comment that includes: + +- Overview of changed files count +- For each file: + - Change type indicator (✨ Added, šŸ“ Modified, šŸ—‘ļø Removed) + - File metadata (root element, controls count, file size) + - List of UI controls used in the file + - Collapsible section with syntax-highlighted XAML content + - Truncated preview (first 30 lines) for readability + +This provides developers with immediate visual feedback on XAML changes without needing to check out the branch locally. \ No newline at end of file diff --git a/.github/scripts/xaml-preview.py b/.github/scripts/xaml-preview.py new file mode 100755 index 0000000000..342429a0b9 --- /dev/null +++ b/.github/scripts/xaml-preview.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python3 +""" +XAML Preview Generator +Processes XAML files and generates formatted previews for PR comments. +""" + +import os +import sys +import xml.etree.ElementTree as ET +import argparse +from pathlib import Path +from typing import List, Dict, Optional + + +def parse_xaml_file(file_path: str) -> Dict: + """Parse XAML file and extract useful information.""" + try: + tree = ET.parse(file_path) + root = tree.getroot() + + # Extract basic information + info = { + 'root_element': root.tag, + 'attributes': dict(root.attrib), + 'children_count': len(list(root)), + 'has_content': bool(root.text and root.text.strip()), + 'namespaces': {}, + 'controls': [], + 'file_size': os.path.getsize(file_path), + 'line_count': 0 + } + + # Count lines + with open(file_path, 'r', encoding='utf-8') as f: + info['line_count'] = len(f.readlines()) + + # Extract namespaces + for key, value in root.attrib.items(): + if key.startswith('xmlns'): + namespace_name = key.split(':', 1)[1] if ':' in key else 'default' + info['namespaces'][namespace_name] = value + + # Extract child controls (simplified) + for child in root.iter(): + if child.tag != root.tag: + control_name = child.tag.split('}')[-1] if '}' in child.tag else child.tag + if control_name not in info['controls']: + info['controls'].append(control_name) + + return info + + except ET.ParseError as e: + return { + 'error': f'XML Parse Error: {str(e)}', + 'file_size': os.path.getsize(file_path) if os.path.exists(file_path) else 0, + 'line_count': 0 + } + except Exception as e: + return { + 'error': f'Error: {str(e)}', + 'file_size': os.path.getsize(file_path) if os.path.exists(file_path) else 0, + 'line_count': 0 + } + + +def format_file_info(file_path: str, info: Dict, change_type: str) -> str: + """Format file information for display.""" + icon_map = { + 'added': '✨', + 'modified': 'šŸ“', + 'removed': 'šŸ—‘ļø' + } + + icon = icon_map.get(change_type, 'šŸ“„') + relative_path = file_path + + if 'error' in info: + return f"## {icon} {change_type.title()}: `{relative_path}`\n\nāš ļø **Error processing file:** {info['error']}\n\n" + + # File size formatting + size = info['file_size'] + if size > 1024 * 1024: + size_str = f"{size / (1024 * 1024):.1f} MB" + elif size > 1024: + size_str = f"{size / 1024:.1f} KB" + else: + size_str = f"{size} bytes" + + # Build summary + summary_parts = [] + + # Clean up root element name for better readability + root_element = info.get('root_element', 'Unknown') + if '}' in root_element: + root_element = root_element.split('}')[-1] + summary_parts.append(f"**Root Element:** `{root_element}`") + + summary_parts.append(f"**Controls:** {len(info.get('controls', []))} types") + summary_parts.append(f"**Size:** {size_str} ({info['line_count']} lines)") + + if info.get('controls'): + controls_list = ', '.join(f"`{ctrl}`" for ctrl in info['controls'][:10]) + if len(info['controls']) > 10: + controls_list += f" and {len(info['controls']) - 10} more" + summary_parts.append(f"**Used Controls:** {controls_list}") + + # Show namespaces if interesting + namespaces = info.get('namespaces', {}) + if namespaces and len(namespaces) > 1: # More than just default namespace + ns_list = [] + for ns, uri in namespaces.items(): + if ns != 'default' and 'spacestation14.io' not in uri: + ns_list.append(f"`{ns}`") + if ns_list: + summary_parts.append(f"**Custom Namespaces:** {', '.join(ns_list[:5])}") + + summary = '\n'.join(f"- {part}" for part in summary_parts) + + # Read file content for preview + content_preview = "" + try: + with open(file_path, 'r', encoding='utf-8') as f: + lines = f.readlines() + preview_lines = lines[:30] # Show first 30 lines + content_preview = ''.join(preview_lines) + if len(lines) > 30: + content_preview += f"\n... (showing first 30 of {len(lines)} lines)" + except Exception as e: + content_preview = f"Error reading file: {str(e)}" + + return f"""## {icon} {change_type.title()}: `{relative_path}` + +{summary} + +
šŸ“‹ Click to view XAML content + +```xml +{content_preview} +``` + +
+ +""" + + +def process_xaml_files(modified_files: List[str], added_files: List[str], removed_files: List[str]) -> str: + """Process all XAML files and generate preview content.""" + + # Filter to only include XAML files + modified_xaml = [f for f in modified_files if f.endswith('.xaml')] + added_xaml = [f for f in added_files if f.endswith('.xaml')] + removed_xaml = [f for f in removed_files if f.endswith('.xaml')] + + preview_content = "# šŸŽØ XAML Preview Bot\n\n" + + total_files = len(modified_xaml) + len(added_xaml) + len(removed_xaml) + + if total_files == 0: + return preview_content + "No XAML files were changed in this PR.\n" + + preview_content += f"Found **{total_files}** XAML file(s) changed in this PR:\n\n" + + # Process added files + for file_path in added_xaml: + if os.path.exists(file_path): + info = parse_xaml_file(file_path) + preview_content += format_file_info(file_path, info, 'added') + else: + preview_content += f"## ✨ Added: `{file_path}`\n\nāš ļø **File not found in current checkout**\n\n" + + # Process modified files + for file_path in modified_xaml: + if os.path.exists(file_path): + info = parse_xaml_file(file_path) + preview_content += format_file_info(file_path, info, 'modified') + else: + preview_content += f"## šŸ“ Modified: `{file_path}`\n\nāš ļø **File not found in current checkout**\n\n" + + # Process removed files + for file_path in removed_xaml: + preview_content += f"## šŸ—‘ļø Removed: `{file_path}`\n\n" + + # Add footer + preview_content += "\n---\n" + preview_content += "*This preview was automatically generated by the XAML Preview Bot*" + + return preview_content + + +def main(): + parser = argparse.ArgumentParser(description='Generate XAML file previews') + parser.add_argument('--modified', default='', help='Space-separated list of modified files') + parser.add_argument('--added', default='', help='Space-separated list of added files') + parser.add_argument('--removed', default='', help='Space-separated list of removed files') + + args = parser.parse_args() + + # Parse file lists + modified_files = [f.strip() for f in args.modified.split() if f.strip()] + added_files = [f.strip() for f in args.added.split() if f.strip()] + removed_files = [f.strip() for f in args.removed.split() if f.strip()] + + # Generate preview content + preview_content = process_xaml_files(modified_files, added_files, removed_files) + + # Output for GitHub Actions + print("PREVIEW_CONTENT<> $GITHUB_OUTPUT + exit 0 + fi + + echo "Processing XAML files: $xaml_files" + python3 ./.github/scripts/xaml-preview.py \ + --modified "${{ steps.files.outputs.modified }}" \ + --added "${{ steps.files.outputs.added }}" \ + --removed "${{ steps.files.outputs.removed }}" \ + >> $GITHUB_OUTPUT + + - name: Find existing comment + uses: peter-evans/find-comment@v1 + id: fc + with: + issue-number: ${{ github.event.number }} + comment-author: 'github-actions[bot]' + body-includes: šŸŽØ XAML Preview Bot + + - name: Create comment if it doesn't exist + if: steps.fc.outputs.comment-id == '' && steps.preview.outputs.skip_preview != 'true' + uses: peter-evans/create-or-update-comment@v1 + with: + issue-number: ${{ github.event.number }} + body: | + ${{ steps.preview.outputs.PREVIEW_CONTENT }} + + - name: Update comment if it exists + if: steps.fc.outputs.comment-id != '' && steps.preview.outputs.skip_preview != 'true' + uses: peter-evans/create-or-update-comment@v1 + with: + comment-id: ${{ steps.fc.outputs.comment-id }} + edit-mode: replace + body: | + ${{ steps.preview.outputs.PREVIEW_CONTENT }} + + - name: Update comment to read that it has been edited + if: steps.fc.outputs.comment-id != '' && steps.preview.outputs.skip_preview != 'true' + uses: peter-evans/create-or-update-comment@v1 + with: + comment-id: ${{ steps.fc.outputs.comment-id }} + edit-mode: append + body: | + + Edit: preview updated after ${{ github.event.pull_request.head.sha }} \ No newline at end of file