Add XAML preview workflow for automated UI change visualization (#2946)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: VigersRay <60344369+VigersRay@users.noreply.github.com>
This commit is contained in:
Copilot 2025-08-22 22:12:40 +03:00 committed by GitHub
parent c592f83188
commit f017405375
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 343 additions and 0 deletions

44
.github/scripts/README.md vendored Normal file
View file

@ -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.

213
.github/scripts/xaml-preview.py vendored Executable file
View file

@ -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}
<details><summary>📋 Click to view XAML content</summary>
```xml
{content_preview}
```
</details>
"""
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<<EOF")
print(preview_content)
print("EOF")
if __name__ == '__main__':
main()

86
.github/workflows/xaml-preview.yml vendored Normal file
View file

@ -0,0 +1,86 @@
name: XAML Preview
on:
pull_request_target:
paths:
- '**.xaml'
jobs:
preview:
name: Generate XAML Previews
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4.2.2
- name: Get changed files
id: files
uses: Ana06/get-changed-files@v2.3.0
with:
format: 'space-delimited'
filter: |
**.xaml
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Generate XAML previews
id: preview
run: |
# Check if any XAML files were actually changed
xaml_files=""
for file in ${{ steps.files.outputs.modified }} ${{ steps.files.outputs.added }} ${{ steps.files.outputs.removed }}; do
if [[ "$file" == *.xaml ]]; then
xaml_files="$xaml_files $file"
fi
done
if [ -z "$xaml_files" ]; then
echo "No XAML files changed, skipping preview generation"
echo "skip_preview=true" >> $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 }}