# Task ID: 101 # Title: Implement GitHub Issue Export Feature with Bidirectional Linking # Status: pending # Dependencies: None # Priority: high # Description: Add a 'github-export' command that creates GitHub issues from Task Master tasks and establishes bidirectional linking between tasks and issues. This complements the import feature by enabling full GitHub integration workflow. # Details: ## Core Problem Statement Users need the ability to export Task Master tasks to GitHub issues to: 1. **Share Tasks with Team**: Convert internal tasks to GitHub issues for team collaboration 2. **Track Progress Publicly**: Make task progress visible in GitHub project boards 3. **Integrate with GitHub Workflow**: Connect Task Master planning with GitHub development workflow 4. **Maintain Synchronization**: Keep tasks and issues linked for status updates 5. **Enable Hybrid Workflow**: Allow teams to work with both Task Master and GitHub seamlessly ## Core Requirements ### 1. GitHub Export Command - **Command**: `task-master github-export --id= --repo= [options]` - **Functionality**: Create GitHub issue from Task Master task - **Authentication**: Use GitHub Personal Access Token or OAuth - **Repository Target**: Support any accessible GitHub repository ### 2. Bidirectional Linking System - **Task → Issue**: Store GitHub issue URL in task metadata - **Issue → Task**: Include Task Master reference in GitHub issue description - **Link Validation**: Verify links remain valid and accessible - **Link Display**: Show GitHub links in task views and vice versa ### 3. Content Mapping and Formatting - **Title Mapping**: Task title → GitHub issue title - **Description Mapping**: Task description → GitHub issue description - **Details Conversion**: Convert Task Master details to GitHub markdown - **Metadata Preservation**: Include Task Master ID, priority, status in issue - **Subtask Handling**: Convert subtasks to GitHub issue checklist or separate issues ### 4. Advanced Export Options - **Selective Export**: Choose which task fields to include - **Template Customization**: Custom GitHub issue templates - **Label Management**: Map Task Master priorities/tags to GitHub labels - **Assignee Mapping**: Map Task Master assignments to GitHub assignees - **Milestone Integration**: Connect tasks to GitHub milestones ## Technical Implementation Requirements ### 1. GitHub API Integration ```javascript // Core export service class GitHubExportService { constructor(token, baseURL = 'https://api.github.com') { this.token = token; this.baseURL = baseURL; this.rateLimiter = new RateLimiter(); } async exportTask(task, repoOwner, repoName, options = {}) { // Validate repository access // Format task content for GitHub // Create GitHub issue via API // Update task with GitHub link // Return export result } async updateTaskWithGitHubLink(taskId, issueUrl) { // Add GitHub link to task metadata // Update task file with link reference // Regenerate task files if needed } } ``` ### 2. Content Formatting System ```javascript class TaskToGitHubFormatter { formatIssueTitle(task) { return `[Task ${task.id}] ${task.title}`; } formatIssueDescription(task) { let description = `# ${task.title}\n\n`; description += `**Task Master ID**: ${task.id}\n`; description += `**Priority**: ${task.priority}\n`; description += `**Status**: ${task.status}\n\n`; if (task.description) { description += `## Description\n${task.description}\n\n`; } if (task.details) { description += `## Implementation Details\n${task.details}\n\n`; } if (task.subtasks && task.subtasks.length > 0) { description += `## Subtasks\n`; task.subtasks.forEach(subtask => { const checked = subtask.status === 'done' ? 'x' : ' '; description += `- [${checked}] ${subtask.title}\n`; }); } description += `\n---\n*Exported from Task Master*`; return description; } } ``` ### 3. Bidirectional Link Management ```javascript class LinkManager { async addGitHubLinkToTask(taskId, issueUrl, issueNumber) { const task = await getTask(taskId); if (!task.metadata) task.metadata = {}; task.metadata.githubIssue = { url: issueUrl, number: issueNumber, exportedAt: new Date().toISOString(), repository: this.extractRepoFromUrl(issueUrl) }; await updateTask(taskId, task); await regenerateTaskFiles(); } async validateGitHubLink(issueUrl) { // Check if GitHub issue still exists // Verify access permissions // Return link status } generateTaskMasterReference(taskId, projectName) { return `\n\n---\n**Task Master Reference**: Task #${taskId} in project "${projectName}"`; } } ``` ### 4. Command Line Interface ```javascript // In commands.js program .command('github-export') .description('Export Task Master task to GitHub issue') .requiredOption('-i, --id ', 'Task ID to export') .requiredOption('-r, --repo ', 'Target GitHub repository') .option('-t, --token ', 'GitHub Personal Access Token (or use GITHUB_TOKEN env var)') .option('--title ', 'Override issue title') .option('--labels <labels>', 'Comma-separated list of GitHub labels') .option('--assignees <assignees>', 'Comma-separated list of GitHub usernames') .option('--milestone <milestone>', 'GitHub milestone number or title') .option('--template <template>', 'Custom issue template file') .option('--include-subtasks', 'Export subtasks as checklist items') .option('--separate-subtasks', 'Create separate issues for subtasks') .option('--dry-run', 'Preview the issue content without creating it') .option('--force', 'Overwrite existing GitHub link if present') .action(async (options) => { await handleGitHubExport(options); }); ``` ### 5. MCP Tool Integration ```javascript // MCP tool for github-export export function registerGitHubExportTool(server) { server.addTool({ name: "github_export_task", description: "Export a Task Master task to GitHub issue with bidirectional linking", parameters: { type: "object", properties: { taskId: { type: "string", description: "Task ID to export" }, repository: { type: "string", description: "GitHub repository (owner/repo)" }, token: { type: "string", description: "GitHub Personal Access Token" }, options: { type: "object", properties: { title: { type: "string", description: "Override issue title" }, labels: { type: "array", items: { type: "string" } }, assignees: { type: "array", items: { type: "string" } }, milestone: { type: "string", description: "Milestone number or title" }, includeSubtasks: { type: "boolean", description: "Include subtasks as checklist" }, separateSubtasks: { type: "boolean", description: "Create separate issues for subtasks" }, dryRun: { type: "boolean", description: "Preview without creating" } } } }, required: ["taskId", "repository"] }, execute: async (args) => { return await gitHubExportDirect(args); } }); } ``` ## Advanced Features ### 1. Batch Export - Export multiple tasks at once - Maintain relationships between exported issues - Progress tracking for bulk operations - Rollback capability for failed exports ### 2. Synchronization Features - **Status Sync**: Update Task Master when GitHub issue status changes - **Comment Sync**: Sync comments between Task Master and GitHub - **Webhook Integration**: Real-time updates via GitHub webhooks - **Conflict Resolution**: Handle conflicting updates gracefully ### 3. Template System ```javascript // Custom export templates const issueTemplates = { bug: { title: "[BUG] {task.title}", labels: ["bug", "task-master"], body: `## Bug Description\n{task.description}\n\n## Steps to Reproduce\n{task.details}` }, feature: { title: "[FEATURE] {task.title}", labels: ["enhancement", "task-master"], body: `## Feature Request\n{task.description}\n\n## Implementation Details\n{task.details}` } }; ``` ### 4. Integration with GitHub Projects - Automatically add exported issues to GitHub project boards - Map Task Master status to GitHub project columns - Sync priority levels with GitHub project priorities ## Error Handling and Edge Cases ### 1. Authentication Issues - Invalid or expired GitHub tokens - Insufficient repository permissions - Rate limiting and quota management ### 2. Repository Issues - Non-existent repositories - Private repository access - Repository permission changes ### 3. Content Issues - Task content too large for GitHub issue - Invalid characters in titles or descriptions - Markdown formatting conflicts ### 4. Link Management Issues - Broken or invalid GitHub links - Deleted GitHub issues - Repository transfers or renames ## Testing Strategy ### 1. Unit Tests - GitHub API client functionality - Content formatting and conversion - Link management operations - Error handling scenarios ### 2. Integration Tests - End-to-end export workflow - Bidirectional linking verification - GitHub API integration - Authentication flow testing ### 3. Performance Tests - Bulk export operations - Rate limiting compliance - Large task content handling - Concurrent export operations ## Security Considerations ### 1. Token Management - Secure storage of GitHub tokens - Token validation and refresh - Scope limitation and permissions - Environment variable protection ### 2. Data Privacy - Sensitive information filtering - Private repository handling - User consent for public exports - Audit logging for exports ## Documentation Requirements ### 1. User Guide - Setup and authentication instructions - Export workflow examples - Troubleshooting common issues - Best practices for GitHub integration ### 2. API Documentation - MCP tool reference - CLI command documentation - Configuration options - Integration examples ### 3. Developer Guide - Extension points for custom templates - Webhook setup instructions - Advanced configuration options - Contributing guidelines # Test Strategy: # Subtasks: ## 1. Implement GitHub API Export Service [pending] ### Dependencies: None ### Description: Create the core service for exporting tasks to GitHub issues via the GitHub REST API ### Details: ## Implementation Requirements ### Core GitHub Export Service ```javascript // scripts/modules/github/github-export-service.js class GitHubExportService { constructor(token, options = {}) { this.token = token; this.baseURL = options.baseURL || 'https://api.github.com'; this.rateLimiter = new RateLimiter({ tokensPerInterval: 5000, // GitHub API limit interval: 'hour' }); } async exportTask(task, repoOwner, repoName, exportOptions = {}) { // Validate repository access await this.validateRepositoryAccess(repoOwner, repoName); // Format task content for GitHub const issueData = this.formatTaskAsIssue(task, exportOptions); // Create GitHub issue const issue = await this.createGitHubIssue(repoOwner, repoName, issueData); // Update task with GitHub link await this.updateTaskWithGitHubLink(task.id, issue.html_url, issue.number); return { success: true, issue: issue, taskId: task.id, issueUrl: issue.html_url }; } } ``` ### Repository Validation - **Access Check**: Verify user has write access to target repository - **Repository Existence**: Confirm repository exists and is accessible - **Permission Validation**: Check if user can create issues in the repository - **Rate Limit Check**: Ensure API quota is available for the operation ### Issue Creation Logic ```javascript async createGitHubIssue(owner, repo, issueData) { const response = await this.makeAPIRequest('POST', `/repos/${owner}/${repo}/issues`, { title: issueData.title, body: issueData.body, labels: issueData.labels || [], assignees: issueData.assignees || [], milestone: issueData.milestone || null }); if (!response.ok) { throw new GitHubAPIError(`Failed to create issue: ${response.statusText}`); } return response.json(); } ``` ### Error Handling - **Authentication Errors**: Invalid or expired tokens - **Permission Errors**: Insufficient repository access - **Rate Limiting**: Handle API quota exceeded - **Network Errors**: Connection timeouts and failures - **Validation Errors**: Invalid repository or issue data ### Testing Requirements - Unit tests for API client methods - Mock GitHub API responses for testing - Error scenario testing (invalid repos, auth failures) - Rate limiting behavior verification - Integration tests with real GitHub API (using test repositories) ## 2. Create Task-to-GitHub Content Formatter [pending] ### Dependencies: 101.1 ### Description: Implement intelligent content formatting to convert Task Master tasks into properly formatted GitHub issues ### Details: ## Implementation Requirements ### Core Formatting Service ```javascript // scripts/modules/github/task-formatter.js class TaskToGitHubFormatter { constructor(options = {}) { this.options = { includeTaskId: true, includeMetadata: true, convertSubtasksToChecklist: true, addTaskMasterReference: true, ...options }; } formatTaskAsIssue(task, exportOptions = {}) { return { title: this.formatTitle(task, exportOptions), body: this.formatBody(task, exportOptions), labels: this.formatLabels(task, exportOptions), assignees: this.formatAssignees(task, exportOptions) }; } } ``` ### Title Formatting ```javascript formatTitle(task, options) { let title = task.title; // Add task ID prefix if enabled if (this.options.includeTaskId && !options.hideTaskId) { title = `[Task ${task.id}] ${title}`; } // Add priority indicator for high priority tasks if (task.priority === 'high') { title = `🔥 ${title}`; } // Truncate if too long (GitHub limit is 256 characters) if (title.length > 250) { title = title.substring(0, 247) + '...'; } return title; } ``` ### Body Formatting ```javascript formatBody(task, options) { let body = ''; // Header with task metadata if (this.options.includeMetadata) { body += this.formatMetadataSection(task); } // Main description if (task.description) { body += `## Description\n\n${task.description}\n\n`; } // Implementation details if (task.details) { body += `## Implementation Details\n\n${this.formatDetails(task.details)}\n\n`; } // Test strategy if (task.testStrategy) { body += `## Test Strategy\n\n${task.testStrategy}\n\n`; } // Subtasks as checklist if (task.subtasks && task.subtasks.length > 0 && this.options.convertSubtasksToChecklist) { body += this.formatSubtasksSection(task.subtasks); } // Dependencies if (task.dependencies && task.dependencies.length > 0) { body += this.formatDependenciesSection(task.dependencies); } // Task Master reference if (this.options.addTaskMasterReference) { body += this.formatTaskMasterReference(task); } return body; } ``` ### Metadata Section ```javascript formatMetadataSection(task) { let metadata = '## Task Information\n\n'; metadata += `| Field | Value |\n`; metadata += `|-------|-------|\n`; metadata += `| **Task ID** | ${task.id} |\n`; metadata += `| **Priority** | ${this.formatPriority(task.priority)} |\n`; metadata += `| **Status** | ${this.formatStatus(task.status)} |\n`; if (task.dependencies && task.dependencies.length > 0) { metadata += `| **Dependencies** | ${task.dependencies.join(', ')} |\n`; } if (task.complexityScore) { metadata += `| **Complexity** | ${task.complexityScore}/10 |\n`; } metadata += '\n'; return metadata; } ``` ### Subtasks Formatting ```javascript formatSubtasksSection(subtasks) { let section = '## Subtasks\n\n'; subtasks.forEach(subtask => { const checked = subtask.status === 'done' ? 'x' : ' '; section += `- [${checked}] **${subtask.title}**`; if (subtask.description) { section += ` - ${subtask.description}`; } section += '\n'; // Add subtask details as indented content if (subtask.details) { const indentedDetails = subtask.details .split('\n') .map(line => ` ${line}`) .join('\n'); section += `${indentedDetails}\n`; } }); section += '\n'; return section; } ``` ### Label Generation ```javascript formatLabels(task, options) { const labels = []; // Always add task-master label labels.push('task-master'); // Priority-based labels if (task.priority === 'high') { labels.push('priority:high'); } else if (task.priority === 'low') { labels.push('priority:low'); } // Status-based labels if (task.status === 'in-progress') { labels.push('in-progress'); } // Complexity-based labels if (task.complexityScore >= 8) { labels.push('complexity:high'); } else if (task.complexityScore <= 3) { labels.push('complexity:low'); } // Custom labels from options if (options.labels) { labels.push(...options.labels); } return labels; } ``` ### Markdown Conversion ```javascript formatDetails(details) { // Convert Task Master specific formatting to GitHub markdown let formatted = details; // Convert code blocks formatted = formatted.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => { return `\`\`\`${lang || ''}\n${code}\`\`\``; }); // Convert inline code formatted = formatted.replace(/`([^`]+)`/g, '`$1`'); // Convert headers formatted = formatted.replace(/^(#{1,6})\s+(.+)$/gm, '$1 $2'); // Convert lists formatted = formatted.replace(/^\s*[-*+]\s+(.+)$/gm, '- $1'); // Convert numbered lists formatted = formatted.replace(/^\s*\d+\.\s+(.+)$/gm, (match, content, offset, string) => { const lineNumber = string.substring(0, offset).split('\n').length; return `${lineNumber}. ${content}`; }); return formatted; } ``` ### Task Master Reference ```javascript formatTaskMasterReference(task) { return `\n---\n\n*This issue was exported from Task Master*\n\n` + `**Original Task**: #${task.id}\n` + `**Exported**: ${new Date().toISOString()}\n` + `**Task Master Project**: ${this.getProjectName()}\n`; } ``` ### Template System ```javascript class IssueTemplateManager { constructor() { this.templates = { default: new DefaultTemplate(), bug: new BugTemplate(), feature: new FeatureTemplate(), epic: new EpicTemplate() }; } applyTemplate(task, templateName, options) { const template = this.templates[templateName] || this.templates.default; return template.format(task, options); } } class BugTemplate extends TaskToGitHubFormatter { formatTitle(task, options) { return `🐛 [BUG] ${task.title}`; } formatBody(task, options) { let body = '## Bug Report\n\n'; body += `**Task ID**: ${task.id}\n\n`; if (task.description) { body += `### Description\n${task.description}\n\n`; } if (task.details) { body += `### Steps to Reproduce\n${task.details}\n\n`; } body += `### Expected Behavior\n<!-- Describe what should happen -->\n\n`; body += `### Actual Behavior\n<!-- Describe what actually happens -->\n\n`; return body + this.formatTaskMasterReference(task); } } ``` ### Testing Requirements - Unit tests for all formatting methods - Test with various task structures (with/without subtasks, different priorities) - Markdown conversion accuracy testing - Template system testing - Character limit and truncation testing - Special character handling (emojis, unicode) - Large content handling and performance testing ## 3. Implement Bidirectional Link Management System [pending] ### Dependencies: 101.2 ### Description: Create a robust system for managing links between Task Master tasks and GitHub issues, including validation and synchronization ### Details: ## Implementation Requirements ### Core Link Management Service ```javascript // scripts/modules/github/link-manager.js class GitHubLinkManager { constructor(githubService) { this.githubService = githubService; this.linkCache = new Map(); } async addGitHubLinkToTask(taskId, issueUrl, issueNumber, repository) { const task = await this.getTask(taskId); // Initialize metadata if it doesn't exist if (!task.metadata) { task.metadata = {}; } // Add GitHub link information task.metadata.githubIssue = { url: issueUrl, number: issueNumber, repository: repository, exportedAt: new Date().toISOString(), lastValidated: new Date().toISOString(), status: 'active' }; // Update task in storage await this.updateTask(taskId, task); // Regenerate task files to include the link await this.regenerateTaskFiles(); // Cache the link for quick access this.linkCache.set(taskId, task.metadata.githubIssue); return task.metadata.githubIssue; } } ``` ### Task Metadata Schema ```javascript // Enhanced task structure with GitHub integration const taskWithGitHubLink = { id: 42, title: "Example Task", description: "Task description", // ... other task fields ... metadata: { githubIssue: { url: "https://github.com/owner/repo/issues/123", number: 123, repository: "owner/repo", exportedAt: "2024-01-15T10:30:00.000Z", lastValidated: "2024-01-15T10:30:00.000Z", status: "active", // active, closed, deleted, invalid syncEnabled: true, lastSyncAt: "2024-01-15T10:30:00.000Z" }, // Other metadata fields... } }; ``` ### Link Validation System ```javascript class LinkValidator { constructor(githubService) { this.githubService = githubService; } async validateGitHubLink(taskId, linkInfo) { try { const { repository, number } = linkInfo; const [owner, repo] = repository.split('/'); // Check if issue still exists const issue = await this.githubService.getIssue(owner, repo, number); if (!issue) { return { valid: false, status: 'deleted', message: 'GitHub issue no longer exists' }; } // Check if issue is closed const status = issue.state === 'open' ? 'active' : 'closed'; // Update link status if changed if (linkInfo.status !== status) { await this.updateLinkStatus(taskId, status); } return { valid: true, status: status, issue: issue, lastValidated: new Date().toISOString() }; } catch (error) { if (error.status === 404) { return { valid: false, status: 'deleted', message: 'GitHub issue not found' }; } else if (error.status === 403) { return { valid: false, status: 'access_denied', message: 'Access denied to GitHub issue' }; } throw error; } } async validateAllLinks() { const tasks = await this.getAllTasksWithGitHubLinks(); const results = []; for (const task of tasks) { if (task.metadata?.githubIssue) { const result = await this.validateGitHubLink(task.id, task.metadata.githubIssue); results.push({ taskId: task.id, ...result }); } } return results; } } ``` ### Task File Enhancement ```javascript // Enhanced task file generation with GitHub links class TaskFileGenerator { generateTaskFile(task) { let content = this.generateBasicTaskContent(task); // Add GitHub integration section if link exists if (task.metadata?.githubIssue) { content += this.generateGitHubSection(task.metadata.githubIssue); } return content; } generateGitHubSection(githubInfo) { let section = '\n## GitHub Integration\n\n'; section += `**GitHub Issue**: [#${githubInfo.number}](${githubInfo.url})\n`; section += `**Repository**: ${githubInfo.repository}\n`; section += `**Status**: ${this.formatGitHubStatus(githubInfo.status)}\n`; section += `**Exported**: ${new Date(githubInfo.exportedAt).toLocaleDateString()}\n`; if (githubInfo.lastValidated) { section += `**Last Validated**: ${new Date(githubInfo.lastValidated).toLocaleDateString()}\n`; } if (githubInfo.status === 'closed') { section += '\n> ⚠️ **Note**: The linked GitHub issue has been closed.\n'; } else if (githubInfo.status === 'deleted') { section += '\n> ❌ **Warning**: The linked GitHub issue no longer exists.\n'; } return section; } formatGitHubStatus(status) { const statusMap = { 'active': '🟢 Active', 'closed': '🔴 Closed', 'deleted': '❌ Deleted', 'invalid': '⚠️ Invalid', 'access_denied': '🔒 Access Denied' }; return statusMap[status] || status; } } ``` ### GitHub Issue Reference System ```javascript class GitHubReferenceManager { generateTaskMasterReference(taskId, projectName, taskUrl = null) { let reference = '\n\n---\n\n'; reference += '**🔗 Task Master Integration**\n\n'; reference += `- **Task ID**: #${taskId}\n`; reference += `- **Project**: ${projectName}\n`; reference += `- **Exported**: ${new Date().toISOString()}\n`; if (taskUrl) { reference += `- **Task URL**: [View in Task Master](${taskUrl})\n`; } reference += '\n*This issue is managed by Task Master. Changes made here may be overwritten during synchronization.*\n'; return reference; } async updateGitHubIssueWithTaskReference(issueUrl, taskId, projectName) { const { owner, repo, number } = this.parseGitHubUrl(issueUrl); const issue = await this.githubService.getIssue(owner, repo, number); if (!issue) { throw new Error('GitHub issue not found'); } // Check if Task Master reference already exists const hasReference = issue.body.includes('Task Master Integration'); if (!hasReference) { const reference = this.generateTaskMasterReference(taskId, projectName); const updatedBody = issue.body + reference; await this.githubService.updateIssue(owner, repo, number, { body: updatedBody }); } } } ``` ### Link Synchronization ```javascript class LinkSynchronizer { constructor(githubService, linkManager) { this.githubService = githubService; this.linkManager = linkManager; } async syncTaskWithGitHubIssue(taskId) { const task = await this.getTask(taskId); const githubInfo = task.metadata?.githubIssue; if (!githubInfo || !githubInfo.syncEnabled) { return { synced: false, reason: 'Sync not enabled' }; } const { repository, number } = githubInfo; const [owner, repo] = repository.split('/'); try { const issue = await this.githubService.getIssue(owner, repo, number); if (!issue) { await this.linkManager.updateLinkStatus(taskId, 'deleted'); return { synced: false, reason: 'Issue deleted' }; } // Sync status changes const changes = await this.detectChanges(task, issue); if (changes.length > 0) { await this.applyChanges(taskId, changes); await this.linkManager.updateLastSync(taskId); return { synced: true, changes: changes, lastSync: new Date().toISOString() }; } return { synced: true, changes: [] }; } catch (error) { console.error(`Failed to sync task ${taskId}:`, error); return { synced: false, error: error.message }; } } async detectChanges(task, issue) { const changes = []; // Check if GitHub issue was closed and task is still pending if (issue.state === 'closed' && task.status !== 'done') { changes.push({ type: 'status', from: task.status, to: 'done', reason: 'GitHub issue closed' }); } // Check if GitHub issue was reopened and task is done if (issue.state === 'open' && task.status === 'done') { changes.push({ type: 'status', from: task.status, to: 'in-progress', reason: 'GitHub issue reopened' }); } return changes; } } ``` ### CLI Integration ```javascript // Add link management commands program .command('github-link') .description('Manage GitHub links for tasks') .option('--validate', 'Validate all GitHub links') .option('--sync <taskId>', 'Sync specific task with GitHub') .option('--sync-all', 'Sync all linked tasks') .option('--remove <taskId>', 'Remove GitHub link from task') .action(async (options) => { if (options.validate) { await validateAllGitHubLinks(); } else if (options.sync) { await syncTaskWithGitHub(options.sync); } else if (options.syncAll) { await syncAllTasksWithGitHub(); } else if (options.remove) { await removeGitHubLink(options.remove); } }); ``` ### Testing Requirements - Unit tests for link management operations - Integration tests with GitHub API - Link validation testing (valid, invalid, deleted issues) - Synchronization testing with various scenarios - Error handling testing (network failures, auth issues) - Performance testing with large numbers of linked tasks - Cache behavior testing - Concurrent operation testing ## 4. Create CLI and MCP Tool Integration [pending] ### Dependencies: 101.3 ### Description: Implement the command-line interface and MCP tools for GitHub export functionality ### Details: ## Implementation Requirements ### CLI Command Implementation ```javascript // In scripts/modules/commands.js program .command('github-export') .description('Export Task Master task to GitHub issue with bidirectional linking') .requiredOption('-i, --id <taskId>', 'Task ID to export') .requiredOption('-r, --repo <owner/repo>', 'Target GitHub repository (owner/repo format)') .option('-t, --token <token>', 'GitHub Personal Access Token (or use GITHUB_TOKEN env var)') .option('--title <title>', 'Override the GitHub issue title') .option('--labels <labels>', 'Comma-separated list of GitHub labels to add') .option('--assignees <assignees>', 'Comma-separated list of GitHub usernames to assign') .option('--milestone <milestone>', 'GitHub milestone number or title') .option('--template <template>', 'Issue template to use (bug, feature, epic, default)') .option('--include-subtasks', 'Include subtasks as checklist items in the issue') .option('--separate-subtasks', 'Create separate GitHub issues for each subtask') .option('--dry-run', 'Preview the issue content without actually creating it') .option('--force', 'Overwrite existing GitHub link if task is already linked') .option('--no-link-back', 'Do not add Task Master reference to the GitHub issue') .option('--sync', 'Enable automatic synchronization between task and issue') .action(async (options) => { try { await handleGitHubExport(options); } catch (error) { console.error(chalk.red('GitHub export failed:'), error.message); process.exit(1); } }); ``` ### Core Export Handler ```javascript // scripts/modules/github/github-export-handler.js async function handleGitHubExport(options) { const { id: taskId, repo: repository, token, title: titleOverride, labels, assignees, milestone, template = 'default', includeSubtasks, separateSubtasks, dryRun, force, linkBack = true, sync = false } = options; // Validate inputs await validateExportOptions(options); // Get task details const task = await getTask(taskId); if (!task) { throw new Error(`Task ${taskId} not found`); } // Check for existing GitHub link if (task.metadata?.githubIssue && !force) { const existingUrl = task.metadata.githubIssue.url; console.log(chalk.yellow(`Task ${taskId} is already linked to GitHub issue: ${existingUrl}`)); console.log(chalk.gray('Use --force to overwrite the existing link')); return; } // Initialize GitHub service const githubToken = token || process.env.GITHUB_TOKEN; if (!githubToken) { throw new Error('GitHub token required. Use --token flag or set GITHUB_TOKEN environment variable'); } const githubService = new GitHubExportService(githubToken); const formatter = new TaskToGitHubFormatter(); const linkManager = new GitHubLinkManager(githubService); // Format task content const exportOptions = { titleOverride, labels: labels ? labels.split(',').map(l => l.trim()) : [], assignees: assignees ? assignees.split(',').map(a => a.trim()) : [], milestone, template, includeSubtasks, linkBack }; const issueData = formatter.formatTaskAsIssue(task, exportOptions); // Dry run - just show what would be created if (dryRun) { console.log(chalk.cyan('\\n=== DRY RUN - GitHub Issue Preview ===\\n')); console.log(chalk.bold('Title:'), issueData.title); console.log(chalk.bold('\\nLabels:'), issueData.labels.join(', ') || 'None'); console.log(chalk.bold('\\nAssignees:'), issueData.assignees.join(', ') || 'None'); console.log(chalk.bold('\\nBody:')); console.log(issueData.body); console.log(chalk.cyan('\\n=== End Preview ===\\n')); return; } // Show progress console.log(chalk.blue(`Exporting task ${taskId} to GitHub repository ${repository}...`)); // Export to GitHub const result = await githubService.exportTask(task, repository, exportOptions); if (result.success) { console.log(chalk.green(`✅ Successfully exported task ${taskId} to GitHub!`)); console.log(chalk.cyan(`GitHub Issue: ${result.issueUrl}`)); // Add bidirectional link await linkManager.addGitHubLinkToTask(taskId, result.issueUrl, result.issue.number, repository); if (linkBack) { await linkManager.updateGitHubIssueWithTaskReference(result.issueUrl, taskId, getProjectName()); } if (sync) { await linkManager.enableSync(taskId); console.log(chalk.blue('🔄 Synchronization enabled for this task')); } // Handle subtasks if requested if (separateSubtasks && task.subtasks && task.subtasks.length > 0) { console.log(chalk.blue('\\nExporting subtasks as separate issues...')); await exportSubtasksAsSeparateIssues(task.subtasks, repository, githubService, linkManager); } } else { throw new Error(result.error || 'Export failed'); } } ``` ### MCP Tool Implementation ```javascript // mcp-server/src/tools/github-export.js import { githubExportDirect } from '../core/direct-functions/github-export-direct.js'; import { handleApiResult, withNormalizedProjectRoot } from './utils.js'; export function registerGitHubExportTool(server) { server.addTool({ name: "github_export_task", description: "Export a Task Master task to GitHub issue with bidirectional linking", parameters: { type: "object", properties: { taskId: { type: "string", description: "Task ID to export (required)" }, repository: { type: "string", description: "GitHub repository in owner/repo format (required)" }, token: { type: "string", description: "GitHub Personal Access Token (optional if GITHUB_TOKEN env var is set)" }, options: { type: "object", properties: { title: { type: "string", description: "Override the GitHub issue title" }, labels: { type: "array", items: { type: "string" }, description: "GitHub labels to add to the issue" }, assignees: { type: "array", items: { type: "string" }, description: "GitHub usernames to assign to the issue" }, milestone: { type: "string", description: "GitHub milestone number or title" }, template: { type: "string", enum: ["default", "bug", "feature", "epic"], description: "Issue template to use" }, includeSubtasks: { type: "boolean", description: "Include subtasks as checklist items" }, separateSubtasks: { type: "boolean", description: "Create separate issues for subtasks" }, dryRun: { type: "boolean", description: "Preview without creating the issue" }, force: { type: "boolean", description: "Overwrite existing GitHub link" }, linkBack: { type: "boolean", description: "Add Task Master reference to GitHub issue" }, enableSync: { type: "boolean", description: "Enable automatic synchronization" } } }, projectRoot: { type: "string", description: "Project root directory path" } }, required: ["taskId", "repository"] }, execute: withNormalizedProjectRoot(async (args, { log, session }) => { try { const result = await githubExportDirect(args, log, { session }); return handleApiResult(result, log); } catch (error) { log(`GitHub export error: ${error.message}`); return { success: false, error: error.message }; } }) }); } ``` ### Direct Function Implementation ```javascript // mcp-server/src/core/direct-functions/github-export-direct.js import { handleGitHubExport } from '../../../../scripts/modules/github/github-export-handler.js'; import { createLogWrapper } from '../../tools/utils.js'; export async function githubExportDirect(args, log, context = {}) { const { session } = context; const mcpLog = createLogWrapper(log); try { // Prepare options for the core handler const options = { id: args.taskId, repo: args.repository, token: args.token, ...args.options, projectRoot: args.projectRoot }; // Call the core export handler const result = await handleGitHubExport(options, { session, mcpLog, outputFormat: 'json' // Request JSON output for MCP }); return { success: true, data: { taskId: args.taskId, repository: args.repository, issueUrl: result.issueUrl, issueNumber: result.issue.number, exportedAt: new Date().toISOString(), message: `Successfully exported task ${args.taskId} to GitHub issue #${result.issue.number}` } }; } catch (error) { mcpLog(`GitHub export failed: ${error.message}`); return { success: false, error: error.message }; } } ``` ### Validation Functions ```javascript // scripts/modules/github/validation.js async function validateExportOptions(options) { const { id: taskId, repo: repository, token } = options; // Validate task ID if (!taskId || !/^\\d+(\\.\\d+)*$/.test(taskId)) { throw new Error('Invalid task ID format'); } // Validate repository format if (!repository || !/^[a-zA-Z0-9._-]+\\/[a-zA-Z0-9._-]+$/.test(repository)) { throw new Error('Repository must be in owner/repo format'); } // Validate GitHub token const githubToken = token || process.env.GITHUB_TOKEN; if (!githubToken) { throw new Error('GitHub token is required'); } if (!/^gh[ps]_[a-zA-Z0-9]{36,}$/.test(githubToken)) { console.warn(chalk.yellow('Warning: GitHub token format appears invalid')); } // Validate labels format if (options.labels) { const labels = options.labels.split(',').map(l => l.trim()); for (const label of labels) { if (label.length > 50) { throw new Error(`Label "${label}" is too long (max 50 characters)`); } } } // Validate assignees format if (options.assignees) { const assignees = options.assignees.split(',').map(a => a.trim()); for (const assignee of assignees) { if (!/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(assignee)) { throw new Error(`Invalid GitHub username: ${assignee}`); } } } } ``` ### Help Integration ```javascript // Add to help system const githubExportHelp = { command: 'github-export', description: 'Export Task Master task to GitHub issue', usage: 'task-master github-export --id=<taskId> --repo=<owner/repo> [options]', examples: [ { command: 'task-master github-export --id=42 --repo=myorg/myproject', description: 'Export task 42 to GitHub repository' }, { command: 'task-master github-export --id=42 --repo=myorg/myproject --labels="bug,urgent" --assignees="john,jane"', description: 'Export with custom labels and assignees' }, { command: 'task-master github-export --id=42 --repo=myorg/myproject --dry-run', description: 'Preview the GitHub issue without creating it' }, { command: 'task-master github-export --id=42 --repo=myorg/myproject --template=bug --sync', description: 'Export using bug template with sync enabled' } ], options: [ { flag: '--id <taskId>', description: 'Task ID to export (required)' }, { flag: '--repo <owner/repo>', description: 'GitHub repository (required)' }, { flag: '--token <token>', description: 'GitHub Personal Access Token' }, { flag: '--title <title>', description: 'Override issue title' }, { flag: '--labels <labels>', description: 'Comma-separated labels' }, { flag: '--assignees <users>', description: 'Comma-separated assignees' }, { flag: '--milestone <milestone>', description: 'GitHub milestone' }, { flag: '--template <template>', description: 'Issue template (bug, feature, epic)' }, { flag: '--include-subtasks', description: 'Include subtasks as checklist' }, { flag: '--separate-subtasks', description: 'Create separate issues for subtasks' }, { flag: '--dry-run', description: 'Preview without creating' }, { flag: '--force', description: 'Overwrite existing GitHub link' }, { flag: '--no-link-back', description: 'Skip Task Master reference in issue' }, { flag: '--sync', description: 'Enable automatic synchronization' } ] }; ``` ### Testing Requirements - Unit tests for CLI option parsing and validation - Integration tests for MCP tool functionality - End-to-end tests with real GitHub repositories - Error handling tests for various failure scenarios - Dry-run functionality testing - Template system testing - Subtask export testing (both checklist and separate issues) - Authentication and authorization testing - Rate limiting and retry logic testing ## 5. Create Comprehensive Testing Suite and Documentation [pending] ### Dependencies: 101.4 ### Description: Implement thorough testing for the GitHub export system and create comprehensive documentation ### Details: ## Implementation Requirements ### Testing Strategy #### 1. Unit Tests ```javascript // tests/unit/github-export.test.js describe('GitHub Export System', () => { describe('GitHubExportService', () => { test('should validate repository access', async () => { const service = new GitHubExportService('mock-token'); const mockGitHub = jest.spyOn(service, 'validateRepositoryAccess'); await service.exportTask(mockTask, 'owner', 'repo'); expect(mockGitHub).toHaveBeenCalledWith('owner', 'repo'); }); test('should handle authentication errors', async () => { const service = new GitHubExportService('invalid-token'); await expect(service.exportTask(mockTask, 'owner', 'repo')) .rejects.toThrow('Authentication failed'); }); test('should respect rate limits', async () => { const service = new GitHubExportService('valid-token'); const rateLimiter = jest.spyOn(service.rateLimiter, 'removeTokens'); await service.exportTask(mockTask, 'owner', 'repo'); expect(rateLimiter).toHaveBeenCalled(); }); }); describe('TaskToGitHubFormatter', () => { test('should format task title correctly', () => { const formatter = new TaskToGitHubFormatter(); const task = { id: 42, title: 'Test Task', priority: 'high' }; const result = formatter.formatTitle(task); expect(result).toBe('🔥 [Task 42] Test Task'); }); test('should truncate long titles', () => { const formatter = new TaskToGitHubFormatter(); const longTitle = 'A'.repeat(300); const task = { id: 1, title: longTitle }; const result = formatter.formatTitle(task); expect(result.length).toBeLessThanOrEqual(250); expect(result).toEndWith('...'); }); test('should format subtasks as checklist', () => { const formatter = new TaskToGitHubFormatter(); const task = { id: 1, title: 'Parent Task', subtasks: [ { title: 'Subtask 1', status: 'done' }, { title: 'Subtask 2', status: 'pending' } ] }; const result = formatter.formatBody(task); expect(result).toContain('- [x] **Subtask 1**'); expect(result).toContain('- [ ] **Subtask 2**'); }); test('should generate appropriate labels', () => { const formatter = new TaskToGitHubFormatter(); const task = { priority: 'high', complexityScore: 9 }; const labels = formatter.formatLabels(task); expect(labels).toContain('task-master'); expect(labels).toContain('priority:high'); expect(labels).toContain('complexity:high'); }); }); describe('GitHubLinkManager', () => { test('should add GitHub link to task metadata', async () => { const linkManager = new GitHubLinkManager(mockGitHubService); const taskId = '42'; const issueUrl = 'https://github.com/owner/repo/issues/123'; await linkManager.addGitHubLinkToTask(taskId, issueUrl, 123, 'owner/repo'); const task = await getTask(taskId); expect(task.metadata.githubIssue).toBeDefined(); expect(task.metadata.githubIssue.url).toBe(issueUrl); expect(task.metadata.githubIssue.number).toBe(123); }); test('should validate GitHub links', async () => { const linkManager = new GitHubLinkManager(mockGitHubService); const linkInfo = { repository: 'owner/repo', number: 123, status: 'active' }; mockGitHubService.getIssue.mockResolvedValue({ state: 'open' }); const result = await linkManager.validateGitHubLink('42', linkInfo); expect(result.valid).toBe(true); expect(result.status).toBe('active'); }); }); }); ``` #### 2. Integration Tests ```javascript // tests/integration/github-export-integration.test.js describe('GitHub Export Integration', () => { let testRepository; let githubToken; beforeAll(() => { githubToken = process.env.GITHUB_TEST_TOKEN; testRepository = process.env.GITHUB_TEST_REPO || 'taskmaster-test/test-repo'; if (!githubToken) { throw new Error('GITHUB_TEST_TOKEN environment variable required for integration tests'); } }); test('should export task to real GitHub repository', async () => { const task = createTestTask(); const service = new GitHubExportService(githubToken); const result = await service.exportTask(task, testRepository, { labels: ['test', 'automated'], template: 'default' }); expect(result.success).toBe(true); expect(result.issueUrl).toMatch(/https:\\/\\/github\\.com\\/.+\\/issues\\/\\d+/); // Cleanup: Close the test issue await service.updateIssue(testRepository, result.issue.number, { state: 'closed' }); }); test('should handle repository permission errors', async () => { const task = createTestTask(); const service = new GitHubExportService(githubToken); await expect(service.exportTask(task, 'private/inaccessible-repo')) .rejects.toThrow(/permission|access/i); }); test('should respect GitHub API rate limits', async () => { const service = new GitHubExportService(githubToken); const tasks = Array.from({ length: 10 }, () => createTestTask()); const startTime = Date.now(); for (const task of tasks) { await service.exportTask(task, testRepository); } const endTime = Date.now(); const duration = endTime - startTime; // Should take some time due to rate limiting expect(duration).toBeGreaterThan(1000); }); }); ``` #### 3. CLI Tests ```javascript // tests/cli/github-export-cli.test.js describe('GitHub Export CLI', () => { test('should validate required options', async () => { const result = await runCLI(['github-export']); expect(result.exitCode).toBe(1); expect(result.stderr).toContain('required option'); }); test('should perform dry run correctly', async () => { const result = await runCLI([ 'github-export', '--id=42', '--repo=owner/repo', '--dry-run' ]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain('DRY RUN'); expect(result.stdout).toContain('GitHub Issue Preview'); }); test('should handle invalid task ID', async () => { const result = await runCLI([ 'github-export', '--id=invalid', '--repo=owner/repo' ]); expect(result.exitCode).toBe(1); expect(result.stderr).toContain('Invalid task ID'); }); test('should validate repository format', async () => { const result = await runCLI([ 'github-export', '--id=42', '--repo=invalid-format' ]); expect(result.exitCode).toBe(1); expect(result.stderr).toContain('owner/repo format'); }); }); ``` #### 4. MCP Tool Tests ```javascript // tests/mcp/github-export-mcp.test.js describe('GitHub Export MCP Tool', () => { test('should export task via MCP', async () => { const args = { taskId: '42', repository: 'owner/repo', token: 'test-token', options: { labels: ['test'], dryRun: true } }; const result = await githubExportDirect(args, mockLog, { session: mockSession }); expect(result.success).toBe(true); expect(result.data.taskId).toBe('42'); }); test('should handle MCP tool errors', async () => { const args = { taskId: 'invalid', repository: 'owner/repo' }; const result = await githubExportDirect(args, mockLog, { session: mockSession }); expect(result.success).toBe(false); expect(result.error).toBeDefined(); }); }); ``` ### Mock Data and Utilities ```javascript // tests/utils/github-mocks.js export function createTestTask() { return { id: Math.floor(Math.random() * 1000), title: 'Test Task', description: 'This is a test task for GitHub export', details: 'Implementation details for the test task', priority: 'medium', status: 'pending', subtasks: [ { title: 'Test Subtask 1', status: 'done' }, { title: 'Test Subtask 2', status: 'pending' } ] }; } export function mockGitHubAPI() { return { getIssue: jest.fn(), createIssue: jest.fn(), updateIssue: jest.fn(), getRepository: jest.fn() }; } export function createMockGitHubResponse(issueNumber = 123) { return { id: issueNumber, number: issueNumber, title: 'Test Issue', body: 'Test issue body', state: 'open', html_url: `https://github.com/owner/repo/issues/${issueNumber}`, created_at: new Date().toISOString(), updated_at: new Date().toISOString() }; } ``` ### Documentation #### 1. User Guide ```markdown # GitHub Export Feature ## Overview The GitHub Export feature allows you to create GitHub issues directly from your Task Master tasks, maintaining bidirectional links between tasks and issues. ## Setup ### 1. GitHub Token Create a GitHub Personal Access Token with the following permissions: - `repo` (for private repositories) - `public_repo` (for public repositories) Set the token as an environment variable: ```bash export GITHUB_TOKEN=your_token_here ``` ### 2. Basic Usage ```bash # Export a task to GitHub task-master github-export --id=42 --repo=myorg/myproject # Export with custom labels and assignees task-master github-export --id=42 --repo=myorg/myproject \\ --labels="bug,urgent" --assignees="john,jane" # Preview before creating task-master github-export --id=42 --repo=myorg/myproject --dry-run ``` ## Advanced Features ### Templates Use predefined templates for different issue types: ```bash # Bug report template task-master github-export --id=42 --repo=myorg/myproject --template=bug # Feature request template task-master github-export --id=42 --repo=myorg/myproject --template=feature ``` ### Subtask Handling ```bash # Include subtasks as checklist items task-master github-export --id=42 --repo=myorg/myproject --include-subtasks # Create separate issues for each subtask task-master github-export --id=42 --repo=myorg/myproject --separate-subtasks ``` ### Synchronization ```bash # Enable automatic synchronization task-master github-export --id=42 --repo=myorg/myproject --sync ``` ## Troubleshooting ### Common Issues 1. **Authentication Error**: Verify your GitHub token has the correct permissions 2. **Repository Not Found**: Ensure the repository exists and you have access 3. **Rate Limit Exceeded**: Wait for the rate limit to reset or use a different token ### Link Management ```bash # Validate all GitHub links task-master github-link --validate # Sync specific task with GitHub task-master github-link --sync 42 # Remove GitHub link from task task-master github-link --remove 42 ``` ``` #### 2. API Documentation ```markdown # GitHub Export API Reference ## MCP Tool: github_export_task ### Parameters - `taskId` (string, required): Task ID to export - `repository` (string, required): GitHub repository in owner/repo format - `token` (string, optional): GitHub Personal Access Token - `options` (object, optional): Export configuration ### Options Object - `title` (string): Override issue title - `labels` (array): GitHub labels to add - `assignees` (array): GitHub usernames to assign - `milestone` (string): GitHub milestone - `template` (string): Issue template (bug, feature, epic, default) - `includeSubtasks` (boolean): Include subtasks as checklist - `separateSubtasks` (boolean): Create separate issues for subtasks - `dryRun` (boolean): Preview without creating - `force` (boolean): Overwrite existing GitHub link - `linkBack` (boolean): Add Task Master reference to issue - `enableSync` (boolean): Enable automatic synchronization ### Response ```json { "success": true, "data": { "taskId": "42", "repository": "owner/repo", "issueUrl": "https://github.com/owner/repo/issues/123", "issueNumber": 123, "exportedAt": "2024-01-15T10:30:00.000Z", "message": "Successfully exported task 42 to GitHub issue #123" } } ``` ``` ### Performance Testing ```javascript // tests/performance/github-export-performance.test.js describe('GitHub Export Performance', () => { test('should export large task within time limit', async () => { const largeTask = createLargeTask(); // Task with many subtasks and long content const service = new GitHubExportService('test-token'); const startTime = performance.now(); await service.exportTask(largeTask, 'owner/repo'); const endTime = performance.now(); expect(endTime - startTime).toBeLessThan(5000); // Should complete in under 5 seconds }); test('should handle concurrent exports', async () => { const service = new GitHubExportService('test-token'); const tasks = Array.from({ length: 5 }, () => createTestTask()); const promises = tasks.map(task => service.exportTask(task, 'owner/repo')); const results = await Promise.all(promises); results.forEach(result => { expect(result.success).toBe(true); }); }); }); ``` ### Test Configuration ```javascript // jest.config.js additions module.exports = { // ... existing config testEnvironment: 'node', setupFilesAfterEnv: ['<rootDir>/tests/setup/github-setup.js'], testMatch: [ '**/tests/**/*.test.js', '**/tests/**/*.spec.js' ], collectCoverageFrom: [ 'scripts/modules/github/**/*.js', 'mcp-server/src/tools/github-*.js', 'mcp-server/src/core/direct-functions/github-*.js' ], coverageThreshold: { global: { branches: 80, functions: 80, lines: 80, statements: 80 } } }; ``` ### Continuous Integration ```yaml # .github/workflows/github-export-tests.yml name: GitHub Export Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: '18' - run: npm ci - run: npm run test:github-export env: GITHUB_TEST_TOKEN: ${{ secrets.GITHUB_TEST_TOKEN }} GITHUB_TEST_REPO: ${{ secrets.GITHUB_TEST_REPO }} ```