UNPKG

@morodomi/ait3

Version:

AIT³ Development Platform - AI + Ticket + Test + Tool driven development methodology

180 lines (179 loc) 6.83 kB
export class TicketMigrationService { /** * Parse ticket filter string into array of local ticket IDs * Supports formats: "1,3,5", "1-10", "1,3-5" * Always returns local format: ["0001", "0003", "0005"] */ parseTicketFilter(filter) { const result = []; // Split by comma first const parts = filter.split(',').map(s => s.trim()); for (const part of parts) { // Check if it's a range (contains '-') if (part.includes('-') && !part.startsWith('-')) { const [start, end] = part.split('-').map(s => s.trim()); // Extract numbers const startNum = parseInt(start, 10); const endNum = parseInt(end, 10); if (isNaN(startNum) || isNaN(endNum)) { throw new Error(`Invalid range format: ${part}`); } if (startNum > endNum) { throw new Error(`Invalid range: start (${startNum}) must be <= end (${endNum})`); } // Generate range with local format (zero-padded) for (let i = startNum; i <= endNum; i++) { result.push(i.toString().padStart(4, '0')); } } else { // Single ID - convert to local format const num = parseInt(part, 10); if (isNaN(num)) { throw new Error(`Invalid ticket ID format: ${part}`); } result.push(num.toString().padStart(4, '0')); } } return result; } /** * Validate migration between two ticket services */ async validateMigration(from, to) { try { const fromTickets = await from.listTickets(); const toTickets = await to.listTickets(); const conflicts = []; const warnings = []; const errors = []; // Check for ID conflicts const toIds = new Set(toTickets.map(ticket => this.extractIdNumber(ticket.id))); for (const ticket of fromTickets) { const idNumber = this.extractIdNumber(ticket.id); if (toIds.has(idNumber)) { conflicts.push(`ID conflict: local ${ticket.id} vs github #${idNumber}`); } } // Add standard warnings only if there are tickets to migrate if (fromTickets.length > 0) { warnings.push('Local markdown content will be migrated to GitHub issue body'); } return { success: conflicts.length === 0 && errors.length === 0, conflicts, warnings, errors }; } catch { return { success: false, conflicts: [], warnings: [], errors: ['Failed to read local tickets'] }; } } /** * Migrate tickets from local service to GitHub service */ async migrateLocalToGitHub(localService, githubService, ticketFilter) { try { let ticketsToMigrate = []; if (ticketFilter && ticketFilter.length > 0) { // Use provided filter ticketsToMigrate = ticketFilter; } else { // Get all tickets const localTickets = await localService.listTickets(); ticketsToMigrate = localTickets.map(t => t.id); } if (ticketsToMigrate.length === 0) { return { success: true, migratedCount: 0, failedCount: 0, errors: [] }; } let migratedCount = 0; let failedCount = 0; const errors = []; for (const ticketId of ticketsToMigrate) { try { // Get full ticket details including description const fullTicket = await localService.getTicket(ticketId); if (!fullTicket) { failedCount++; errors.push(`Failed to get full details for ticket ${ticketId}`); continue; } const createdTicket = await githubService.createTicket(fullTicket.title, { priority: fullTicket.priority, labels: fullTicket.labels, description: fullTicket.description, assignee: fullTicket.assignee }); // Update status based on original ticket try { if (fullTicket.status === 'done') { await githubService.completeTicket(createdTicket.id); } else if (fullTicket.status === 'doing') { await githubService.startTicket(createdTicket.id); } } catch { // Status update failure should not fail the migration // The ticket was created successfully, just not in the right status } migratedCount++; } catch (error) { failedCount++; errors.push(`Failed to migrate ticket ${ticketId}: ${error instanceof Error ? error.message : String(error)}`); } } return { success: failedCount === 0, migratedCount, failedCount, errors }; } catch (error) { return { success: false, migratedCount: 0, failedCount: 0, errors: [error instanceof Error ? error.message : String(error)] }; } } /** * Get the next available GitHub issue number */ async getNextAvailableGitHubId(githubService) { try { const tickets = await githubService.listTickets(); if (tickets.length === 0) { return 1; } const maxId = Math.max(...tickets.map(ticket => this.extractIdNumber(ticket.id))); return maxId + 1; } catch { return 1; } } /** * Extract numeric ID from ticket ID string */ extractIdNumber(id) { // Handle both local format "0001" and GitHub format "#1" const match = id.match(/\d+/); return match ? parseInt(match[0], 10) : 0; } }