Skip to content

Instantly share code, notes, and snippets.

@thibaudcolas
Created April 2, 2026 16:10
Show Gist options
  • Select an option

  • Save thibaudcolas/63a3dad90d5897a0d86f68bd7e91a201 to your computer and use it in GitHub Desktop.

Select an option

Save thibaudcolas/63a3dad90d5897a0d86f68bd7e91a201 to your computer and use it in GitHub Desktop.
Recap local GH issues by milestone
#!/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