Files

500 lines
18 KiB
Python

#!/usr/bin/env python3
"""
Script to generate GitHub issues from DMARQ roadmap documents.
This script parses the roadmap files and creates structured issue templates
that can be imported to GitHub.
"""
import json
import re
from pathlib import Path
from typing import List, Dict, Any
from dataclasses import dataclass, asdict
@dataclass
class Issue:
"""Represents a GitHub issue."""
title: str
body: str
labels: List[str]
milestone: str = ""
assignees: List[str] = None
def __post_init__(self):
if self.assignees is None:
self.assignees = []
class RoadmapParser:
"""Parser for DMARQ roadmap documents."""
def __init__(self, roadmap_path: Path):
self.roadmap_path = roadmap_path
self.content = roadmap_path.read_text()
self.issues: List[Issue] = []
def parse_security_sprint(self) -> List[Issue]:
"""Parse the Security Remediation Sprint section."""
issues = []
# Pattern to match sections like "#### 1. Authentication & Authorization (CRITICAL)"
section_pattern = r'####\s+(\d+)\.\s+(.+?)\((.+?)\)\s*\n(.*?)(?=####|\n##|\Z)'
for match in re.finditer(section_pattern, self.content, re.DOTALL):
section_num = match.group(1)
title = match.group(2).strip()
priority = match.group(3).strip()
content = match.group(4).strip()
# Extract checklist items
checklist_items = re.findall(r'- \[ \] (.+)', content)
# Extract files to fix
files_section = re.search(r'\*\*Files to Fix\*\*:\s*\n((?:\s*-\s+`.+?`.*\n?)+)', content)
files = []
if files_section:
files = re.findall(r'-\s+`(.+?)`', files_section.group(1))
# Build issue body
body_parts = [
f"## Description",
f"",
f"Part of the **Security Remediation Sprint** - Priority: **{priority}**",
f"",
f"### Tasks",
""
]
for item in checklist_items:
body_parts.append(f"- [ ] {item}")
if files:
body_parts.extend([
"",
"### Files to Update",
""
])
for file in files:
body_parts.append(f"- `{file}`")
body_parts.extend([
"",
"### Related Documentation",
"",
"- [SECURITY.md](../SECURITY.md)",
"- [Security Remediation Sprint](../roadmap.md#security-remediation-sprint-priority---in-progress)",
"",
"---",
"*This issue was auto-generated from the DMARQ roadmap.*"
])
# Determine labels based on priority
labels = ["security"]
if "CRITICAL" in priority:
labels.extend(["priority: critical", "security: critical"])
elif "HIGH" in priority:
labels.extend(["priority: high", "security: high"])
elif "MEDIUM" in priority:
labels.extend(["priority: medium"])
issue = Issue(
title=f"[Security Sprint] {title}",
body="\n".join(body_parts),
labels=labels,
milestone="Security Remediation Sprint"
)
issues.append(issue)
return issues
def parse_milestone_features(self) -> List[Issue]:
"""Parse milestone features from the roadmap."""
issues = []
# Pattern to match milestone sections
milestone_pattern = r'##\s+Milestone\s+(\d+):\s+(.+?)(?:\((.+?)\))?\s*\n(.*?)(?=\n##|\Z)'
for match in re.finditer(milestone_pattern, self.content, re.DOTALL):
milestone_num = match.group(1)
milestone_name = match.group(2).strip()
timeline = match.group(3) or ""
content = match.group(4).strip()
# Skip completed milestones
if "COMPLETE ✅" in milestone_name or "✅" in content[:100]:
continue
# Extract planned features
features_match = re.search(r'###\s+Planned Features\s*\n((?:- \[ \].+\n?)+)', content, re.MULTILINE)
security_match = re.search(r'###\s+Security (?:Features|Considerations|Enhancements Needed)\s*\n((?:- \[ \].+\n?)+)', content, re.MULTILINE)
if features_match:
features = re.findall(r'- \[ \] (.+)', features_match.group(1))
for feature in features:
body_parts = [
f"## Description",
f"",
f"Feature for **Milestone {milestone_num}: {milestone_name}**",
f"",
]
if timeline:
body_parts.append(f"**Timeline**: {timeline}")
body_parts.append("")
body_parts.extend([
f"### Feature",
f"{feature}",
"",
])
# Add security considerations if they exist
if security_match:
security_items = re.findall(r'- \[ \] (.+)', security_match.group(1))
if security_items:
body_parts.extend([
"### Security Considerations",
""
])
for item in security_items:
body_parts.append(f"- [ ] {item}")
body_parts.append("")
body_parts.extend([
"### Related Documentation",
"",
f"- [Milestone {milestone_num}](../roadmap.md#milestone-{milestone_num}-{milestone_name.lower().replace(' ', '-').replace('&', '').replace('(', '').replace(')', '')})",
"",
"---",
"*This issue was auto-generated from the DMARQ roadmap.*"
])
labels = ["enhancement", f"milestone-{milestone_num}"]
# Add priority based on milestone
if int(milestone_num) <= 5:
labels.append("priority: high")
elif int(milestone_num) <= 8:
labels.append("priority: medium")
else:
labels.append("priority: low")
issue = Issue(
title=f"[M{milestone_num}] {feature[:80]}",
body="\n".join(body_parts),
labels=labels,
milestone=f"Milestone {milestone_num}: {milestone_name}"
)
issues.append(issue)
# Create separate issues for security enhancements
if security_match and "Security Enhancements Needed" in content:
security_items = re.findall(r'- \[ \] (.+)', security_match.group(1))
if security_items:
body_parts = [
f"## Description",
f"",
f"Security enhancements for **Milestone {milestone_num}: {milestone_name}**",
f"",
"### Security Tasks",
""
]
for item in security_items:
body_parts.append(f"- [ ] {item}")
body_parts.extend([
"",
"### Related Documentation",
"",
f"- [Milestone {milestone_num}](../roadmap.md#milestone-{milestone_num}-{milestone_name.lower().replace(' ', '-').replace('&', '').replace('(', '').replace(')', '')})",
"- [SECURITY.md](../SECURITY.md)",
"",
"---",
"*This issue was auto-generated from the DMARQ roadmap.*"
])
issue = Issue(
title=f"[M{milestone_num}] Security Enhancements for {milestone_name}",
body="\n".join(body_parts),
labels=["security", f"milestone-{milestone_num}", "enhancement"],
milestone=f"Milestone {milestone_num}: {milestone_name}"
)
issues.append(issue)
return issues
def parse_continuous_improvements(self) -> List[Issue]:
"""Parse continuous improvement tasks."""
issues = []
# Find the Continuous Improvements section
ci_start = self.content.find('## Continuous Improvements')
if ci_start == -1:
return issues
# Find the end at "---" or next "##"
content_after_ci = self.content[ci_start:]
# Find the first separator (--- line by itself)
separator_match = re.search(r'\n---\n', content_after_ci)
next_section_match = re.search(r'\n## [^C]', content_after_ci) # Next section that doesn't start with 'C'
# Take whichever comes first
end_pos = len(content_after_ci)
if separator_match and next_section_match:
end_pos = min(separator_match.start(), next_section_match.start())
elif separator_match:
end_pos = separator_match.start()
elif next_section_match:
end_pos = next_section_match.start()
ci_content = content_after_ci[:end_pos]
# Split by ### to get subsections
subsections = re.split(r'\n###\s+', ci_content)
for subsection in subsections[1:]: # Skip the first split (the title)
lines = subsection.split('\n')
if not lines:
continue
category = lines[0].strip()
items = []
for line in lines[1:]:
match = re.match(r'- \[ \] (.+)', line)
if match:
items.append(match.group(1))
if not items:
continue
body_parts = [
f"## Description",
f"",
f"Ongoing {category.lower()} tasks to maintain and improve DMARQ.",
f"",
"### Tasks",
""
]
for item in items:
body_parts.append(f"- [ ] {item}")
body_parts.extend([
"",
"### Related Documentation",
"",
"- [Continuous Improvements](../roadmap.md#continuous-improvements-ongoing)",
"",
"---",
"*This issue was auto-generated from the DMARQ roadmap.*"
])
labels = ["maintenance", "continuous-improvement"]
if category.lower() == "security":
labels.append("security")
elif category.lower() == "documentation":
labels.append("documentation")
elif category.lower() == "code quality":
labels.append("code-quality")
issue = Issue(
title=f"[Continuous] {category} Improvements",
body="\n".join(body_parts),
labels=labels,
milestone="Continuous Improvements"
)
issues.append(issue)
return issues
def parse_all(self) -> List[Issue]:
"""Parse all issues from the roadmap."""
print(f"Parsing {self.roadmap_path.name}...")
self.issues.extend(self.parse_security_sprint())
print(f" - Found {len(self.issues)} security sprint issues")
milestone_count = len(self.issues)
self.issues.extend(self.parse_milestone_features())
print(f" - Found {len(self.issues) - milestone_count} milestone feature issues")
ci_count = len(self.issues)
self.issues.extend(self.parse_continuous_improvements())
print(f" - Found {len(self.issues) - ci_count} continuous improvement issues")
return self.issues
def save_issues_json(issues: List[Issue], output_path: Path):
"""Save issues to JSON file."""
issues_data = [asdict(issue) for issue in issues]
with output_path.open('w') as f:
json.dump(issues_data, f, indent=2)
print(f"\nSaved {len(issues)} issues to {output_path}")
def save_issues_markdown(issues: List[Issue], output_path: Path):
"""Save issues to Markdown file for easy review."""
with output_path.open('w') as f:
f.write("# DMARQ Roadmap Issues\n\n")
f.write(f"Auto-generated from roadmap documents.\n\n")
f.write(f"**Total Issues**: {len(issues)}\n\n")
f.write("---\n\n")
# Group by milestone
by_milestone = {}
for issue in issues:
milestone = issue.milestone or "No Milestone"
if milestone not in by_milestone:
by_milestone[milestone] = []
by_milestone[milestone].append(issue)
for milestone, milestone_issues in sorted(by_milestone.items()):
f.write(f"## {milestone}\n\n")
f.write(f"**Count**: {len(milestone_issues)}\n\n")
for issue in milestone_issues:
f.write(f"### {issue.title}\n\n")
f.write(f"**Labels**: {', '.join(issue.labels)}\n\n")
f.write("```markdown\n")
f.write(issue.body)
f.write("\n```\n\n")
f.write("---\n\n")
print(f"Saved issues preview to {output_path}")
def save_github_import_script(issues: List[Issue], output_path: Path):
"""Save a bash script to import issues using GitHub CLI."""
script_lines = [
"#!/bin/bash",
"#",
"# Script to create GitHub issues from DMARQ roadmap",
"# Requires: GitHub CLI (gh) - https://cli.github.com/",
"#",
"# Usage: ./create_issues.sh",
"#",
"",
"set -e",
"",
"REPO=\"christianlouis/dmarq\"",
"",
"echo \"Creating GitHub issues for DMARQ roadmap...\"",
"echo \"Repository: $REPO\"",
"echo \"\"",
"",
"# Check if gh is installed",
"if ! command -v gh &> /dev/null; then",
" echo \"Error: GitHub CLI (gh) is not installed.\"",
" echo \"Please install it from: https://cli.github.com/\"",
" exit 1",
"fi",
"",
"# Check if authenticated",
"if ! gh auth status &> /dev/null; then",
" echo \"Error: Not authenticated with GitHub CLI.\"",
" echo \"Please run: gh auth login\"",
" exit 1",
"fi",
"",
"ISSUE_COUNT=0",
""
]
for i, issue in enumerate(issues, 1):
# Escape special characters in title and body
title = issue.title.replace('"', '\\"').replace('$', '\\$')
body = issue.body.replace('"', '\\"').replace('$', '\\$').replace('`', '\\`')
labels = ','.join(issue.labels)
script_lines.extend([
f"# Issue {i}: {issue.title[:50]}...",
f"echo \"Creating issue {i}/{len(issues)}: {title[:50]}...\"",
"gh issue create \\",
f" --repo \"$REPO\" \\",
f" --title \"{title}\" \\",
f" --body \"{body}\" \\",
f" --label \"{labels}\" || echo \" Failed to create issue {i}\"",
"",
"ISSUE_COUNT=$((ISSUE_COUNT + 1))",
"sleep 1 # Rate limiting",
""
])
script_lines.extend([
"echo \"\"",
"echo \"Done! Created $ISSUE_COUNT issues.\"",
"echo \"\"",
"echo \"Next steps:\"",
"echo \"1. Review the created issues at: https://github.com/$REPO/issues\"",
"echo \"2. Create milestones if needed\"",
"echo \"3. Assign issues to milestones and team members\"",
"echo \"4. Start working on the Security Remediation Sprint first!\""
])
script_content = "\n".join(script_lines)
output_path.write_text(script_content)
output_path.chmod(0o755) # Make executable
print(f"Saved GitHub CLI import script to {output_path}")
def main():
"""Main function."""
repo_root = Path(__file__).parent.parent
roadmap_path = repo_root / "docs" / "development" / "roadmap.md"
output_dir = repo_root / "docs" / "development" / "generated_issues"
output_dir.mkdir(exist_ok=True)
print("=" * 60)
print("DMARQ Roadmap Issue Generator")
print("=" * 60)
print()
if not roadmap_path.exists():
print(f"Error: Roadmap file not found at {roadmap_path}")
return 1
# Parse roadmap
parser = RoadmapParser(roadmap_path)
issues = parser.parse_all()
print(f"\n{'=' * 60}")
print(f"Total issues generated: {len(issues)}")
print(f"{'=' * 60}\n")
# Save outputs
save_issues_json(issues, output_dir / "issues.json")
save_issues_markdown(issues, output_dir / "issues_preview.md")
save_github_import_script(issues, output_dir / "create_issues.sh")
print("\n" + "=" * 60)
print("Output files created in:", output_dir)
print("=" * 60)
print("\nFiles generated:")
print(f" 1. issues.json - JSON format for programmatic import")
print(f" 2. issues_preview.md - Human-readable preview")
print(f" 3. create_issues.sh - Executable script using GitHub CLI")
print("\nTo create the issues:")
print(f" cd {output_dir}")
print(" ./create_issues.sh")
print("\nNote: You'll need GitHub CLI (gh) installed and authenticated.")
print(" Install from: https://cli.github.com/")
print()
return 0
if __name__ == "__main__":
exit(main())