Files
emulator/scripts/generate_release_notes.py
Zephyron d0f96e9a30 Add automatic release system
Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-09-21 17:03:22 +10:00

239 lines
9.7 KiB
Python

#!/usr/bin/env python3
"""
Generate release notes for Citron releases
"""
import os
import sys
import subprocess
import json
from datetime import datetime
def run_git_command(cmd):
"""Run a git command and return its output"""
try:
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, check=True)
return result.stdout.strip()
except subprocess.CalledProcessError:
return None
def get_commits_since_last_tag():
"""Get commits since the last tag"""
try:
# Get the last tag
last_tag = run_git_command("git describe --tags --abbrev=0")
if last_tag:
# Get commits since last tag
commits = run_git_command(f"git log {last_tag}..HEAD --oneline --pretty=format:'%s'")
return commits.split('\n') if commits else []
else:
# No tags, get last 20 commits
commits = run_git_command("git log --oneline --max-count=20 --pretty=format:'%s'")
return commits.split('\n') if commits else []
except:
return []
def categorize_commits(commits):
"""Categorize commits by type"""
categories = {
'Features': [],
'Bug Fixes': [],
'Performance': [],
'UI/UX': [],
'Audio': [],
'Graphics': [],
'Input': [],
'Network': [],
'Build/CI': [],
'Documentation': [],
'Other': []
}
for commit in commits:
commit_lower = commit.lower()
# Categorize based on commit message content
if any(word in commit_lower for word in ['feat', 'feature', 'add', 'implement']):
categories['Features'].append(commit)
elif any(word in commit_lower for word in ['fix', 'bug', 'issue', 'crash', 'error']):
categories['Bug Fixes'].append(commit)
elif any(word in commit_lower for word in ['perf', 'optimize', 'speed', 'fast']):
categories['Performance'].append(commit)
elif any(word in commit_lower for word in ['ui', 'gui', 'interface', 'dialog', 'window']):
categories['UI/UX'].append(commit)
elif any(word in commit_lower for word in ['audio', 'sound', 'music']):
categories['Audio'].append(commit)
elif any(word in commit_lower for word in ['graphics', 'render', 'vulkan', 'opengl', 'gpu']):
categories['Graphics'].append(commit)
elif any(word in commit_lower for word in ['input', 'controller', 'keyboard', 'mouse']):
categories['Input'].append(commit)
elif any(word in commit_lower for word in ['network', 'multiplayer', 'online']):
categories['Network'].append(commit)
elif any(word in commit_lower for word in ['build', 'ci', 'cmake', 'docker']):
categories['Build/CI'].append(commit)
elif any(word in commit_lower for word in ['doc', 'readme', 'comment']):
categories['Documentation'].append(commit)
else:
categories['Other'].append(commit)
# Remove empty categories
return {k: v for k, v in categories.items() if v}
def generate_release_notes(version_info, output_format='markdown'):
"""Generate release notes"""
# Get version information
version_name = version_info['version_name']
git_commit_short = version_info['git_commit_short']
git_commit_full = version_info['git_commit_full']
git_branch = version_info['git_branch']
build_date = version_info['build_date']
build_time = version_info['build_time']
# Get project info from environment or defaults
project_url = os.environ.get('CI_PROJECT_URL', 'https://git.citron-emu.org/citron/emulator')
package_registry_url = os.environ.get('PACKAGE_REGISTRY_URL', f'{project_url}/-/packages')
package_dir = os.environ.get('PACKAGE_DIR', f'Citron-Canary/{version_name}')
# Get commits
commits = get_commits_since_last_tag()
categorized_commits = categorize_commits(commits)
if output_format == 'markdown':
return generate_markdown_release_notes(
version_name, git_commit_short, git_commit_full, git_branch,
build_date, build_time, project_url, package_registry_url,
package_dir, categorized_commits
)
elif output_format == 'json':
return generate_json_release_notes(
version_name, git_commit_short, git_commit_full, git_branch,
build_date, build_time, project_url, package_registry_url,
package_dir, categorized_commits
)
else:
raise ValueError(f"Unknown output format: {output_format}")
def generate_markdown_release_notes(version_name, git_commit_short, git_commit_full,
git_branch, build_date, build_time, project_url,
package_registry_url, package_dir, categorized_commits):
"""Generate markdown release notes"""
notes = []
notes.append(f"# Citron {version_name}")
notes.append("")
notes.append(f"**Build Date:** {build_date} {build_time} UTC ")
notes.append(f"**Commit:** [{git_commit_short}]({project_url}/-/commit/{git_commit_full}) ")
notes.append(f"**Branch:** `{git_branch}`")
notes.append("")
# Downloads section
notes.append("## Downloads")
notes.append("")
notes.append("### Linux (AppImage)")
notes.append(f"- **x86_64 (Generic):** [Citron-{version_name}-anylinux-x86_64.AppImage]({package_registry_url}/{package_dir}/Citron-{version_name}-anylinux-x86_64.AppImage)")
notes.append(f"- **x86_64 (SteamDeck):** [Citron-{version_name}-steamdeck-x86_64.AppImage]({package_registry_url}/{package_dir}/Citron-{version_name}-steamdeck-x86_64.AppImage)")
notes.append(f"- **x86_64 (Compatibility):** [Citron-{version_name}-compat-x86_64.AppImage]({package_registry_url}/{package_dir}/Citron-{version_name}-compat-x86_64.AppImage)")
notes.append("")
notes.append("### Android")
notes.append(f"- **ARM64:** [Citron-{version_name}-android-arm64.apk]({package_registry_url}/{package_dir}/Citron-{version_name}-android-arm64.apk)")
notes.append("")
notes.append("### Source Code")
notes.append(f"- **Source Archive:** [citron-{version_name}-source.tar.gz]({package_registry_url}/{package_dir}/citron-{version_name}-source.tar.gz)")
notes.append("")
# Installation instructions
notes.append("## Installation Instructions")
notes.append("")
notes.append("### Linux")
notes.append("1. Download the appropriate AppImage for your system")
notes.append("2. Make it executable: `chmod +x Citron-*.AppImage`")
notes.append("3. Run: `./Citron-*.AppImage`")
notes.append("")
notes.append("### Android")
notes.append("1. Enable \"Install from Unknown Sources\" in your device settings")
notes.append("2. Download and install the APK file")
notes.append("")
# Changes section
if categorized_commits:
notes.append("## Changes")
notes.append("")
for category, commits in categorized_commits.items():
if commits:
notes.append(f"### {category}")
for commit in commits[:10]: # Limit to 10 commits per category
notes.append(f"- {commit}")
notes.append("")
# Footer
notes.append("---")
notes.append(f"*This is an automated build from commit [{git_commit_short}]({project_url}/-/commit/{git_commit_full})*")
return '\n'.join(notes)
def generate_json_release_notes(version_name, git_commit_short, git_commit_full,
git_branch, build_date, build_time, project_url,
package_registry_url, package_dir, categorized_commits):
"""Generate JSON release notes"""
return json.dumps({
'version': version_name,
'build_date': build_date,
'build_time': build_time,
'git_commit_short': git_commit_short,
'git_commit_full': git_commit_full,
'git_branch': git_branch,
'project_url': project_url,
'package_registry_url': package_registry_url,
'package_dir': package_dir,
'changes': categorized_commits,
'downloads': {
'linux': {
'generic': f'Citron-{version_name}-anylinux-x86_64.AppImage',
'steamdeck': f'Citron-{version_name}-steamdeck-x86_64.AppImage',
'compatibility': f'Citron-{version_name}-compat-x86_64.AppImage'
},
'android': {
'arm64': f'Citron-{version_name}-android-arm64.apk'
},
'source': f'citron-{version_name}-source.tar.gz'
}
}, indent=2)
def main():
"""Main function"""
import argparse
parser = argparse.ArgumentParser(description='Generate release notes for Citron')
parser.add_argument('--format', choices=['markdown', 'json'], default='markdown',
help='Output format (default: markdown)')
parser.add_argument('--version-info', help='JSON file with version information')
parser.add_argument('--output', help='Output file (default: stdout)')
args = parser.parse_args()
# Get version information
if args.version_info and os.path.exists(args.version_info):
with open(args.version_info, 'r') as f:
version_info = json.load(f)
else:
# Import version script
sys.path.insert(0, os.path.dirname(__file__))
from get_version import get_version_info
version_info = get_version_info()
# Generate release notes
notes = generate_release_notes(version_info, args.format)
# Output
if args.output:
with open(args.output, 'w') as f:
f.write(notes)
else:
print(notes)
if __name__ == '__main__':
main()