feat: Add script for automated GitHub issue creation with environment variable validation and issue management
This commit is contained in:
@@ -0,0 +1,249 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script to create GitHub issues for LeagueLedger project.
|
||||
"""
|
||||
import requests
|
||||
import os
|
||||
import json
|
||||
import sys
|
||||
import re
|
||||
from typing import Dict, List, Any
|
||||
from dotenv import load_dotenv
|
||||
from pathlib import Path
|
||||
|
||||
# Load environment variables from .env file
|
||||
env_path = Path(__file__).parent / '.env'
|
||||
load_dotenv(dotenv_path=env_path)
|
||||
|
||||
# GitHub repository information
|
||||
GITHUB_API_URL = "https://api.github.com"
|
||||
REPO_OWNER = os.getenv("REPO_OWNER")
|
||||
REPO_NAME = os.getenv("REPO_NAME")
|
||||
|
||||
# GitHub authentication - use a personal access token
|
||||
GITHUB_TOKEN = os.getenv("GITHUB_TOKEN")
|
||||
|
||||
def validate_env():
|
||||
"""Validate that all required environment variables are set."""
|
||||
missing_vars = []
|
||||
if not GITHUB_TOKEN:
|
||||
missing_vars.append("GITHUB_TOKEN")
|
||||
if not REPO_OWNER:
|
||||
missing_vars.append("REPO_OWNER")
|
||||
if not REPO_NAME:
|
||||
missing_vars.append("REPO_NAME")
|
||||
|
||||
if missing_vars:
|
||||
print("Error: The following environment variables are missing in your .env file:")
|
||||
for var in missing_vars:
|
||||
print(f"- {var}")
|
||||
print("\nPlease update your .env file in the scripts directory.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_all_issues():
|
||||
"""Get all open issues from the repository."""
|
||||
url = f"{GITHUB_API_URL}/repos/{REPO_OWNER}/{REPO_NAME}/issues"
|
||||
params = {
|
||||
"state": "open",
|
||||
"per_page": 100
|
||||
}
|
||||
|
||||
headers = {
|
||||
"Authorization": f"token {GITHUB_TOKEN}",
|
||||
"Accept": "application/vnd.github.v3+json"
|
||||
}
|
||||
|
||||
response = requests.get(url, params=params, headers=headers)
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
print(f"Failed to fetch issues: {response.status_code}")
|
||||
print(response.text)
|
||||
return []
|
||||
|
||||
def close_issue(issue_number):
|
||||
"""Close an issue by its number."""
|
||||
url = f"{GITHUB_API_URL}/repos/{REPO_OWNER}/{REPO_NAME}/issues/{issue_number}"
|
||||
|
||||
headers = {
|
||||
"Authorization": f"token {GITHUB_TOKEN}",
|
||||
"Accept": "application/vnd.github.v3+json"
|
||||
}
|
||||
|
||||
data = {
|
||||
"state": "closed"
|
||||
}
|
||||
|
||||
response = requests.patch(url, json=data, headers=headers)
|
||||
|
||||
if response.status_code == 200:
|
||||
print(f"Successfully closed issue #{issue_number}")
|
||||
return True
|
||||
else:
|
||||
print(f"Failed to close issue #{issue_number}: {response.status_code}")
|
||||
print(response.text)
|
||||
return False
|
||||
|
||||
def delete_all_issues():
|
||||
"""Close all open issues in the repository."""
|
||||
issues = get_all_issues()
|
||||
|
||||
if not issues:
|
||||
print("No open issues found.")
|
||||
return
|
||||
|
||||
print(f"Found {len(issues)} open issues. Closing all...")
|
||||
|
||||
for issue in issues:
|
||||
close_issue(issue["number"])
|
||||
|
||||
print("Finished closing issues.")
|
||||
|
||||
def create_github_issue(issue_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Create a GitHub issue using the GitHub API."""
|
||||
url = f"{GITHUB_API_URL}/repos/{REPO_OWNER}/{REPO_NAME}/issues"
|
||||
|
||||
headers = {
|
||||
"Authorization": f"token {GITHUB_TOKEN}",
|
||||
"Accept": "application/vnd.github.v3+json"
|
||||
}
|
||||
|
||||
response = requests.post(url, json=issue_data, headers=headers)
|
||||
|
||||
if response.status_code == 201:
|
||||
print(f"Successfully created issue: {issue_data['title']}")
|
||||
return response.json()
|
||||
else:
|
||||
print(f"Failed to create issue: {response.status_code}")
|
||||
print(response.text)
|
||||
return {}
|
||||
|
||||
def parse_todo_file(file_path):
|
||||
"""Parse the TODO.md file and extract the issues."""
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
except Exception as e:
|
||||
print(f"Error reading TODO.md file: {e}")
|
||||
return []
|
||||
|
||||
# Split the content by sections (## headings)
|
||||
sections = re.split(r'^## ', content, flags=re.MULTILINE)
|
||||
|
||||
# The first section is the title and intro, skip it
|
||||
sections = sections[1:]
|
||||
|
||||
all_issues = []
|
||||
|
||||
for section in sections:
|
||||
# Get the section title from the first line
|
||||
title_line = section.split('\n', 1)[0].strip()
|
||||
section_content = section.split('\n', 1)[1].strip() if len(section.split('\n', 1)) > 1 else ""
|
||||
|
||||
# Create a main issue for the section
|
||||
section_issue = {
|
||||
"title": title_line,
|
||||
"body": f"## {title_line}\n\nThis issue tracks the progress of all {title_line.lower()} tasks.\n\n### Tasks\n{section_content}",
|
||||
"labels": ["epic", title_line.lower().replace(' & ', '-').replace(' ', '-')]
|
||||
}
|
||||
all_issues.append(section_issue)
|
||||
|
||||
# Parse individual tasks in the section
|
||||
tasks = parse_section_tasks(section_content, title_line)
|
||||
all_issues.extend(tasks)
|
||||
|
||||
return all_issues
|
||||
|
||||
def parse_section_tasks(section_content, section_title):
|
||||
"""Parse individual tasks from a section."""
|
||||
issues = []
|
||||
|
||||
# Extract tasks at the top level (directly under the section)
|
||||
tasks = re.findall(r'^- \[ \] (.+)$', section_content, re.MULTILINE)
|
||||
for task in tasks:
|
||||
# Skip tasks that are groups (they have subtasks and usually end with a colon)
|
||||
if task.strip().endswith(':') or task.strip().startswith('**'):
|
||||
continue
|
||||
|
||||
issue = {
|
||||
"title": task.strip(),
|
||||
"body": f"## Description\nImplement the following task from {section_title}:\n\n{task.strip()}\n\n## Related\nPart of the {section_title} epic.",
|
||||
"labels": ["task", section_title.lower().replace(' & ', '-').replace(' ', '-')]
|
||||
}
|
||||
issues.append(issue)
|
||||
|
||||
# Extract grouped tasks (subtasks under headings like "User Management")
|
||||
groups = re.findall(r'- \[ \] \*\*(.*?)\*\*\s*\n((?: - \[ \].*\n)*)', section_content, re.MULTILINE)
|
||||
|
||||
for group_name, group_tasks_text in groups:
|
||||
# Create a group issue
|
||||
group_issue = {
|
||||
"title": f"{group_name}",
|
||||
"body": f"## Description\nImplement {group_name} features for {section_title}.\n\n## Tasks\n{group_tasks_text}\n\n## Related\nPart of the {section_title} epic.",
|
||||
"labels": ["group", section_title.lower().replace(' & ', '-').replace(' ', '-'), group_name.lower().replace(' ', '-')]
|
||||
}
|
||||
issues.append(group_issue)
|
||||
|
||||
# Add individual subtasks
|
||||
subtasks = re.findall(r' - \[ \] (.*)', group_tasks_text)
|
||||
for subtask in subtasks:
|
||||
subtask_issue = {
|
||||
"title": f"{group_name}: {subtask.strip()}",
|
||||
"body": f"## Description\nImplement the following {group_name} feature:\n\n{subtask.strip()}\n\n## Related\nPart of the {group_name} group in the {section_title} epic.",
|
||||
"labels": ["subtask", section_title.lower().replace(' & ', '-').replace(' ', '-'), group_name.lower().replace(' ', '-')]
|
||||
}
|
||||
issues.append(subtask_issue)
|
||||
|
||||
return issues
|
||||
|
||||
def main():
|
||||
"""Main function to create all the issues."""
|
||||
print("LeagueLedger GitHub Issue Creator")
|
||||
print("=================================")
|
||||
|
||||
# Validate environment variables
|
||||
if not validate_env():
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Repository: {REPO_OWNER}/{REPO_NAME}")
|
||||
|
||||
# Delete existing issues
|
||||
delete_option = input("Do you want to close all existing open issues? (y/n): ").lower()
|
||||
if delete_option == 'y':
|
||||
delete_all_issues()
|
||||
|
||||
# Parse TODO.md and create issues
|
||||
todo_path = Path(__file__).parent.parent / 'TODO.md'
|
||||
if not todo_path.exists():
|
||||
print(f"Error: TODO.md file not found at {todo_path}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Parsing TODO.md at {todo_path}...")
|
||||
issues = parse_todo_file(todo_path)
|
||||
|
||||
print(f"Found {len(issues)} issues to create.")
|
||||
create_option = input(f"Do you want to create {len(issues)} GitHub issues? (y/n): ").lower()
|
||||
|
||||
if create_option != 'y':
|
||||
print("Operation cancelled.")
|
||||
sys.exit(0)
|
||||
|
||||
# Create issues
|
||||
created_issues = []
|
||||
for issue in issues:
|
||||
result = create_github_issue(issue)
|
||||
if result:
|
||||
created_issues.append(result)
|
||||
|
||||
# Show results
|
||||
if created_issues:
|
||||
print("\nSuccessfully created issues:")
|
||||
for issue in created_issues:
|
||||
print(f"- #{issue['number']} {issue['title']}: {issue['html_url']}")
|
||||
|
||||
print("\nCompleted creating GitHub issues.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user