Skip to main content
Glama
issue_helper.py14.3 kB
#!/usr/bin/env python """ GitHub Issue Helper Script. This script provides simplified commands for common issue management tasks. """ import argparse import os import subprocess import sys from typing import List, Optional, Tuple def run_command(cmd: List[str], check: bool = True) -> Tuple[int, str, str]: """ Run a command and return exit code, stdout, and stderr. Args: cmd: Command to run as a list of strings check: If True, raise an exception on non-zero exit code Returns: Tuple of (exit_code, stdout, stderr) """ try: proc = subprocess.run( cmd, check=check, capture_output=True, text=True ) return proc.returncode, proc.stdout, proc.stderr except subprocess.CalledProcessError as e: print(f"Error running command: {' '.join(cmd)}") print(f"Exit code: {e.returncode}") print(f"Error: {e.stderr}") if check: sys.exit(e.returncode) return e.returncode, e.stdout, e.stderr def update_issue_status(issue_number: int, status: str) -> None: """ Update an issue's status label. Args: issue_number: GitHub issue number status: New status (prioritized, in-progress, completed, reviewed, archived) """ # Get current labels cmd = ['gh', 'issue', 'view', str(issue_number), '--json', 'labels'] _, stdout, _ = run_command(cmd) import json labels_data = json.loads(stdout) # Find and remove any existing status label status_labels = [label for label in labels_data.get('labels', []) if label.get('name', '').startswith('status:')] for label in status_labels: run_command(['gh', 'issue', 'edit', str(issue_number), '--remove-label', label.get('name')]) # Add the new status label run_command(['gh', 'issue', 'edit', str(issue_number), '--add-label', f'status:{status}']) print(f"Updated issue #{issue_number} status to '{status}'") def get_issue_status(issue_number: int) -> str: """ Get the current status of an issue. Args: issue_number: GitHub issue number Returns: Current status without the 'status:' prefix or empty string if not found """ cmd = ['gh', 'issue', 'view', str(issue_number), '--json', 'labels'] _, stdout, _ = run_command(cmd) import json labels_data = json.loads(stdout) for label in labels_data.get('labels', []): name = label.get('name', '') if name.startswith('status:'): return name.replace('status:', '') return "" def check_issue_status(issue_number: int) -> None: """ Check and display the current status of an issue. Args: issue_number: GitHub issue number """ # Get issue details cmd = ['gh', 'issue', 'view', str(issue_number), '--json', 'title,url,labels'] _, stdout, _ = run_command(cmd) import json issue_data = json.loads(stdout) # Extract status and priority status = "" priority = "" for label in issue_data.get('labels', []): name = label.get('name', '') if name.startswith('status:'): status = name.replace('status:', '') elif name.startswith('priority:'): priority = name.replace('priority:', '') # Display information title = issue_data.get('title', 'Unknown') url = issue_data.get('url', '') print(f"Issue #{issue_number}: {title}") print(f"URL: {url}") print(f"Status: {status or 'Not set'}") print(f"Priority: {priority or 'Not set'}") # Get recent commits for this issue print("\nRecent activity:") cmd = ['git', 'log', '--grep', f"#{issue_number}", '--oneline', '-n', '5'] exit_code, stdout, _ = run_command(cmd, check=False) if exit_code == 0 and stdout.strip(): print("Commits:") for line in stdout.strip().split('\n'): print(f" {line}") else: print("No commit activity found for this issue") # Check for PRs cmd = ['gh', 'pr', 'list', '--search', f"#{issue_number}", '--json', 'number,title,state'] _, stdout, _ = run_command(cmd) import json prs = json.loads(stdout) if prs: print("\nPull Requests:") for pr in prs: print(f" #{pr.get('number')} {pr.get('title')} ({pr.get('state')})") else: print("No pull requests found for this issue") def start_work_on_issue(issue_number: int) -> None: """ Start work on an issue by creating a branch and updating status. Args: issue_number: GitHub issue number """ # Check if issue exists and get its status current_status = get_issue_status(issue_number) if not current_status: print(f"Warning: Issue #{issue_number} not found or has no status label") response = input("Do you want to continue anyway? (y/n): ") if response.lower() != 'y': sys.exit(1) elif current_status != 'prioritized': print(f"Warning: Issue #{issue_number} has status '{current_status}', not 'prioritized'") print(f"It's recommended to only start work on prioritized issues") response = input("Do you want to continue anyway? (y/n): ") if response.lower() != 'y': sys.exit(1) # Get issue title for branch name cmd = ['gh', 'issue', 'view', str(issue_number), '--json', 'title'] _, stdout, _ = run_command(cmd) import json title_data = json.loads(stdout) title = title_data.get('title', '') # Create a sanitized branch name import re branch_title = re.sub(r'[^a-zA-Z0-9_-]', '-', title.lower()) branch_name = f"feature/issue-{issue_number}-{branch_title}" if len(branch_name) > 60: # Keep branch names reasonably sized branch_name = branch_name[:60] # Check if branch already exists _, branches, _ = run_command(['git', 'branch', '--list', branch_name], check=False) if branches.strip(): print(f"Branch '{branch_name}' already exists") response = input("Do you want to check out this existing branch? (y/n): ") if response.lower() == 'y': run_command(['git', 'checkout', branch_name]) print(f"Checked out existing branch: {branch_name}") return else: sys.exit(1) # Create and checkout branch run_command(['git', 'checkout', '-b', branch_name]) print(f"Created and checked out branch: {branch_name}") # Make initial commit to reference the issue readme_path = 'README.md' if os.path.exists(readme_path): # Add a small whitespace change to README to create a commit with open(readme_path, 'a') as f: f.write('\n') run_command(['git', 'add', readme_path]) run_command(['git', 'commit', '-m', f"refs #{issue_number} Start implementing {title}"]) run_command(['git', 'push', '-u', 'origin', branch_name]) print(f"Made initial commit and pushed branch") # Show next steps guidance print("\nNext steps:") print(f"1. Make your changes to implement the issue requirements") print(f"2. Commit with appropriate messages:") print(f" - For work in progress: git commit -m \"implements #{issue_number} ...\"") print(f" - For completed work: git commit -m \"fixes #{issue_number} ...\"") print(f"3. When ready to create a PR: python scripts/issue_helper.py complete {issue_number}") else: print(f"Warning: README.md not found, skipping initial commit") # Update issue status (may be redundant if automation works, but good fallback) update_issue_status(issue_number, 'in-progress') def complete_issue(issue_number: int, branch_name: Optional[str] = None) -> None: """ Create a PR to complete an issue. Args: issue_number: GitHub issue number branch_name: Branch name (if None, will use current branch) """ # Check if issue exists and get its status current_status = get_issue_status(issue_number) if not current_status: print(f"Warning: Issue #{issue_number} not found or has no status label") response = input("Do you want to continue anyway? (y/n): ") if response.lower() != 'y': sys.exit(1) elif current_status != 'in-progress': print(f"Warning: Issue #{issue_number} has status '{current_status}', not 'in-progress'") print(f"It's recommended to only complete issues that are in progress") response = input("Do you want to continue anyway? (y/n): ") if response.lower() != 'y': sys.exit(1) # Get current branch if not provided if branch_name is None: _, branch_name, _ = run_command(['git', 'branch', '--show-current']) branch_name = branch_name.strip() # Verify we're on a feature branch if not branch_name.startswith('feature/') and not branch_name.startswith('bugfix/'): print(f"Warning: Current branch '{branch_name}' doesn't follow the feature/bugfix naming convention") print(f"It's recommended to use the 'start' command to create properly named branches") response = input("Do you want to continue anyway? (y/n): ") if response.lower() != 'y': sys.exit(1) # Check if branch has uncommitted changes _, status, _ = run_command(['git', 'status', '--porcelain']) if status.strip(): print("Warning: You have uncommitted changes:") print(status) response = input("Do you want to commit these changes before creating the PR? (y/n): ") if response.lower() == 'y': message = input(f"Enter commit message (will be prefixed with 'fixes #{issue_number}'): ") run_command(['git', 'add', '.']) run_command(['git', 'commit', '-m', f"fixes #{issue_number} {message}"]) run_command(['git', 'push']) print("Changes committed and pushed") else: print("Continuing with uncommitted changes. They will not be included in the PR.") # Get issue title for PR title cmd = ['gh', 'issue', 'view', str(issue_number), '--json', 'title'] _, stdout, _ = run_command(cmd) import json title_data = json.loads(stdout) title = title_data.get('title', '') # Create PR pr_title = f"Resolves #{issue_number}: {title}" pr_body = f"Closes #{issue_number}" run_command(['gh', 'pr', 'create', '--title', pr_title, '--body', pr_body, '--base', 'main']) print(f"Created PR to complete issue #{issue_number}") # Show next steps guidance print("\nNext steps:") print("1. Wait for the CI checks to pass") print("2. Request a review if needed") print("3. Once approved, the PR can be merged") print("4. After merging, the issue should automatically transition to 'completed' status") def force_update_issue_for_testing(issue_number: int, status: str) -> None: """ Force update an issue's status for testing purposes. This is to simulate what the GitHub Actions workflow would do. Args: issue_number: GitHub issue number status: New status """ update_issue_status(issue_number, status) # Add a comment explaining the forced update comment = f""" This issue status was manually updated to '{status}' for testing purposes. In normal operation, this status change would be triggered by: - For 'in-progress': A commit with 'refs #{issue_number}' or 'implements #{issue_number}' - For 'completed': A PR being merged with 'closes #{issue_number}' or 'fixes #{issue_number}' - For 'reviewed': Currently a manual step after completion - For 'archived': Currently a manual step after review """ run_command(['gh', 'issue', 'comment', str(issue_number), '--body', comment]) print(f"Added comment explaining the forced status update") def main(): parser = argparse.ArgumentParser(description='GitHub Issue Helper') subparsers = parser.add_subparsers(dest='command', help='Command to run') # Start work on issue start_parser = subparsers.add_parser('start', help='Start work on an issue') start_parser.add_argument('issue', type=int, help='Issue number') # Complete issue complete_parser = subparsers.add_parser('complete', help='Complete an issue by creating a PR') complete_parser.add_argument('issue', type=int, help='Issue number') complete_parser.add_argument('--branch', help='Branch name (defaults to current branch)') # Update issue status update_parser = subparsers.add_parser('update', help='Update an issue status') update_parser.add_argument('issue', type=int, help='Issue number') update_parser.add_argument('status', choices=['prioritized', 'in-progress', 'completed', 'reviewed', 'archived'], help='New status') # Force update for testing test_parser = subparsers.add_parser('test', help='Force update issue status for testing') test_parser.add_argument('issue', type=int, help='Issue number') test_parser.add_argument('status', choices=['prioritized', 'in-progress', 'completed', 'reviewed', 'archived'], help='New status') # Check issue status check_parser = subparsers.add_parser('check', help='Check current status of an issue') check_parser.add_argument('issue', type=int, help='Issue number') args = parser.parse_args() if args.command == 'start': start_work_on_issue(args.issue) elif args.command == 'complete': complete_issue(args.issue, args.branch) elif args.command == 'update': update_issue_status(args.issue, args.status) elif args.command == 'test': force_update_issue_for_testing(args.issue, args.status) elif args.command == 'check': check_issue_status(args.issue) else: parser.print_help() if __name__ == '__main__': main()

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/non-dirty/imap-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server