Qwen-Image-Layered-MRP-MLX/scripts/generate_release_notes.py
2025-06-14 11:18:27 +02:00

193 lines
7.0 KiB
Python

#!/usr/bin/env python3
import argparse
import re
import sys
from pathlib import Path
from typing import List, Tuple
class ChangelogParser:
def __init__(self, changelog_path: Path = Path("CHANGELOG.md")):
self.changelog_path = changelog_path
if not self.changelog_path.exists():
raise FileNotFoundError(f"Changelog file not found: {self.changelog_path}")
def extract_changelog_entry(self, version: str) -> str:
"""Extract changelog entry for a specific version"""
content = self.changelog_path.read_text(encoding="utf-8")
# Pattern to match version headers: ## [VERSION]
version_pattern = rf"^## \[{re.escape(version)}\].*$"
next_version_pattern = r"^## \[.*\].*$"
lines = content.splitlines()
# Find the start of the target version section
start_idx = None
for i, line in enumerate(lines):
if re.match(version_pattern, line):
start_idx = i + 1 # Start after the version header
break
if start_idx is None:
raise ValueError(f"Version {version} not found in changelog")
# Find the end of the section (next version header)
end_idx = len(lines)
for i in range(start_idx, len(lines)):
if re.match(next_version_pattern, lines[i]):
end_idx = i
break
# Extract the content between version headers
entry_lines = lines[start_idx:end_idx]
# Clean up: remove empty lines at start/end and strip trailing whitespace
while entry_lines and not entry_lines[0].strip():
entry_lines.pop(0)
while entry_lines and not entry_lines[-1].strip():
entry_lines.pop()
entry_content = "\n".join(line.rstrip() for line in entry_lines)
if not entry_content.strip():
raise ValueError(f"No content found for version {version}")
return entry_content
def list_all_versions(self) -> List[Tuple[str, str]]:
"""List all versions in the changelog with their dates"""
content = self.changelog_path.read_text(encoding="utf-8")
lines = content.splitlines()
versions = []
version_pattern = r"^## \[([^\]]+)\](?:\s*-\s*(.+))?.*$"
for line in lines:
match = re.match(version_pattern, line)
if match:
version = match.group(1)
date = match.group(2) if match.group(2) else "No date"
if version.lower() != "unreleased": # Skip "Unreleased" section
versions.append((version, date))
return versions
def validate_changelog_format(self) -> List[str]:
"""Validate changelog format and return list of issues"""
content = self.changelog_path.read_text(encoding="utf-8")
lines = content.splitlines()
issues = []
# Check if file starts with proper header
if not lines or not lines[0].strip().startswith("# Changelog"):
issues.append("Changelog should start with '# Changelog' header")
# Check for proper version format
version_pattern = r"^## \[([^\]]+)\](?:\s*-\s*(.+))?.*$"
found_versions = []
for i, line in enumerate(lines):
if re.match(r"^## \[", line):
match = re.match(version_pattern, line)
if not match:
issues.append(f"Line {i + 1}: Invalid version header format: {line}")
else:
version = match.group(1)
found_versions.append(version)
# Check version format (except for "Unreleased")
if version.lower() != "unreleased":
if not re.match(r"^\d+\.\d+\.\d+", version):
issues.append(f"Line {i + 1}: Version should follow semantic versioning: {version}")
if not found_versions:
issues.append("No version sections found in changelog")
return issues
def get_latest_version(self) -> str:
"""Get the latest version from changelog (excluding Unreleased)"""
versions = self.list_all_versions()
if not versions:
raise ValueError("No versions found in changelog")
return versions[0][0] # First version in list (should be latest)
def main():
parser = argparse.ArgumentParser(description="MFLUX Changelog Parser and Release Notes Generator")
parser.add_argument("--changelog", default="CHANGELOG.md", help="Path to changelog file")
subparsers = parser.add_subparsers(dest="command", help="Available commands")
# Show changelog entry command
show_parser = subparsers.add_parser("show", help="Show changelog entry for a version")
show_parser.add_argument("version", help="Version to show (e.g., 0.8.0)")
show_parser.add_argument("--markdown", action="store_true", help="Output in markdown format")
# List versions command
subparsers.add_parser("list", help="List all versions in changelog")
# Validate command
subparsers.add_parser("validate", help="Validate changelog format")
# Latest version command
subparsers.add_parser("latest", help="Show latest version")
args = parser.parse_args()
if not args.command:
parser.print_help()
return
try:
changelog_parser = ChangelogParser(Path(args.changelog))
if args.command == "show":
entry = changelog_parser.extract_changelog_entry(args.version)
if args.markdown:
print(f"# Release {args.version}\n")
print(entry)
print(f"\n---\n*Changelog entry: {len(entry)} characters*")
else:
print(f"📝 Changelog entry for version {args.version}:")
print("=" * 60)
print(entry)
print("=" * 60)
print(f"✅ Found changelog entry ({len(entry)} characters)")
elif args.command == "list":
versions = changelog_parser.list_all_versions()
print("📋 Available versions in changelog:")
print("=" * 50)
for version, date in versions:
print(f" {version:<15} - {date}")
print(f"\n✅ Found {len(versions)} versions")
elif args.command == "validate":
issues = changelog_parser.validate_changelog_format()
if issues:
print("❌ Changelog validation failed:")
for issue in issues:
print(f"{issue}")
sys.exit(1)
else:
print("✅ Changelog format is valid")
elif args.command == "latest":
latest = changelog_parser.get_latest_version()
print(f"📦 Latest version: {latest}")
except (ValueError, FileNotFoundError) as e:
print(f"❌ Error: {e}")
sys.exit(1)
except (OSError, ImportError, KeyboardInterrupt) as e:
print(f"❌ Unexpected error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()