Created
April 2, 2026 16:10
-
-
Save thibaudcolas/63a3dad90d5897a0d86f68bd7e91a201 to your computer and use it in GitHub Desktop.
Recap local GH issues by milestone
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env -S uv run | |
| # /// script | |
| # dependencies = [ | |
| # "python-frontmatter>=1.0", | |
| # ] | |
| # /// | |
| """ | |
| Generate a recap of issues assigned to specific milestones from the .issues/closed directory. | |
| This script parses Markdown files with YAML frontmatter and generates a formatted | |
| Markdown list grouped by milestone. | |
| """ | |
| import frontmatter | |
| import re | |
| from pathlib import Path | |
| from typing import Dict, List, Optional, Tuple | |
| def extract_summary(content: str) -> str: | |
| """ | |
| Extract the first paragraph from the content after frontmatter. | |
| Returns a single-line summary suitable for display. | |
| """ | |
| # Remove leading/trailing whitespace and newlines | |
| content = content.strip() | |
| # Split into paragraphs (separated by blank lines) | |
| paragraphs = re.split(r'\n\s*\n', content) | |
| # Find the first non-empty paragraph that's not a heading | |
| for paragraph in paragraphs: | |
| paragraph = paragraph.strip() | |
| if paragraph and not paragraph.startswith('#') and not paragraph.startswith('<'): | |
| # Clean up the paragraph - remove newlines and extra spaces | |
| return ' '.join(paragraph.split()) | |
| return "No summary available" | |
| def parse_issue_file(filepath: Path) -> Optional[Dict]: | |
| """ | |
| Parse a single issue Markdown file and return relevant data. | |
| Returns None if the file doesn't have a milestone we're interested in. | |
| """ | |
| try: | |
| post = frontmatter.load(filepath) | |
| # Get milestone | |
| milestone = post.get('milestone', '') | |
| # Only process specific milestones | |
| target_milestones = {'v6.4', 'v7.0', 'v7.1', 'v7.2', 'v7.3'} | |
| if milestone not in target_milestones: | |
| return None | |
| # Get issue number from filename | |
| match = re.match(r'(\d+)-', filepath.name) | |
| issue_number = match.group(1) if match else None | |
| # Get title | |
| title = post.get('title', 'Untitled') | |
| # Get summary from content | |
| summary = extract_summary(post.content) | |
| # Check if sponsored | |
| labels = post.get('labels', []) | |
| is_sponsored = 'sponsored' in labels or 'sponsored' in post.content.lower() | |
| return { | |
| 'number': issue_number, | |
| 'title': title, | |
| 'summary': summary, | |
| 'milestone': milestone, | |
| 'is_sponsored': is_sponsored, | |
| } | |
| except Exception as e: | |
| print(f"Warning: Could not parse {filepath}: {e}") | |
| return None | |
| def generate_recap(issues_dir: Path) -> str: | |
| """ | |
| Generate the recap markdown output. | |
| """ | |
| # Collect all issues grouped by milestone | |
| milestones: Dict[str, List[Dict]] = { | |
| 'v6.4': [], | |
| 'v7.0': [], | |
| 'v7.1': [], | |
| 'v7.2': [], | |
| 'v7.3': [], | |
| } | |
| # Parse all markdown files in the directory | |
| for md_file in sorted(issues_dir.glob('*.md')): | |
| issue = parse_issue_file(md_file) | |
| if issue: | |
| milestones[issue['milestone']].append(issue) | |
| # Generate output | |
| output_lines = [] | |
| output_lines.append("## Release Recap\n") | |
| for milestone in ['v6.4', 'v7.0', 'v7.1', 'v7.2', 'v7.3']: | |
| output_lines.append(f"\n### {milestone}\n") | |
| issues = milestones.get(milestone, []) | |
| if not issues: | |
| output_lines.append("_No issues for this milestone._\n") | |
| continue | |
| for issue in issues: | |
| # Build the link - using the roadmap repo URL format | |
| if issue['number']: | |
| link = f"https://github.com/wagtail/roadmap/issues/{issue['number']}" | |
| line = f"- [{issue['title']}]({link}) - {issue['summary']}" | |
| else: | |
| line = f"- {issue['title']} - {issue['summary']}" | |
| if issue['is_sponsored']: | |
| line += " (sponsored)" | |
| output_lines.append(line) | |
| output_lines.append("") # Empty line after each milestone | |
| return '\n'.join(output_lines) | |
| def main(): | |
| # Path to the closed issues directory | |
| script_dir = Path(__file__).parent | |
| issues_dir = script_dir / '.issues' / 'closed' | |
| if not issues_dir.exists(): | |
| print(f"Error: Directory not found: {issues_dir}") | |
| return 1 | |
| output = generate_recap(issues_dir) | |
| print(output) | |
| return 0 | |
| if __name__ == '__main__': | |
| exit(main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment