@@ -3,9 +3,11 @@ package cmd
33import (
44 "context"
55 "encoding/csv"
6+ "encoding/json"
67 "fmt"
78 "os"
89 "path/filepath"
10+ "regexp"
911 "strings"
1012 "time"
1113
2628 jiraIssueType string
2729 jiraSearchLabel string // defaults to "flaky_test" if empty
2830 flakyTestJSONDBPath string
31+ assigneeMappingPath string // New flag: path to JSON file for assignee mapping
2932)
3033
34+ // AssigneeMapping holds a regex pattern and its corresponding assignee.
35+ type AssigneeMapping struct {
36+ Pattern string `json:"pattern"`
37+ Assignee string `json:"assignee"`
38+ }
39+
3140var CreateTicketsCmd = & cobra.Command {
3241 Use : "create-tickets" ,
3342 Short : "Interactive TUI to confirm and create Jira tickets from CSV" ,
@@ -42,7 +51,10 @@ ticket in a text-based UI. Press 'y' to confirm creation, 'n' to skip,
4251- A local JSON "database" (via internal/localdb) remembers any tickets
4352 already mapped to tests, so you won't be prompted again in the future.
4453- After the TUI ends, a new CSV is produced, omitting any confirmed rows.
45- The original CSV remains untouched.` ,
54+ The original CSV remains untouched.
55+ - Optionally, an assignee mapping file (JSON) can be provided to set the ticket’s assignee
56+ based on the test package. The mapping supports regex.
57+ ` ,
4658 RunE : func (cmd * cobra.Command , args []string ) error {
4759 // 1) Validate input
4860 if csvPath == "" {
@@ -107,6 +119,34 @@ ticket in a text-based UI. Press 'y' to confirm creation, 'n' to skip,
107119 return nil
108120 }
109121
122+ // Load assignee mapping (if provided)
123+ var mappings []AssigneeMapping
124+ if assigneeMappingPath != "" {
125+ mappingData , err := os .ReadFile (assigneeMappingPath )
126+ if err != nil {
127+ log .Warn ().Err (err ).Msg ("Failed to read assignee mapping file; proceeding without assignee mapping." )
128+ } else {
129+ if err := json .Unmarshal (mappingData , & mappings ); err != nil {
130+ log .Warn ().Err (err ).Msg ("Failed to unmarshal assignee mapping; proceeding without assignee mapping." )
131+ } else {
132+ // Apply mapping: iterate over tickets and assign based on regex match.
133+ for i := range tickets {
134+ for _ , mapping := range mappings {
135+ re , err := regexp .Compile (mapping .Pattern )
136+ if err != nil {
137+ log .Warn ().Msgf ("Invalid regex pattern %q: %v" , mapping .Pattern , err )
138+ continue
139+ }
140+ if re .MatchString (tickets [i ].TestPackage ) {
141+ tickets [i ].Assignee = mapping .Assignee
142+ break // use first matching mapping
143+ }
144+ }
145+ }
146+ }
147+ }
148+ }
149+
110150 // 5) Attempt Jira client creation
111151 client , clientErr := jirautils .GetJiraClient ()
112152 if clientErr != nil {
@@ -179,6 +219,7 @@ func init() {
179219 CreateTicketsCmd .Flags ().StringVar (& jiraIssueType , "jira-issue-type" , "Task" , "Type of Jira issue (Task, Bug, etc.)" )
180220 CreateTicketsCmd .Flags ().StringVar (& jiraSearchLabel , "jira-search-label" , "" , "Jira label to filter existing tickets (default: flaky_test)" )
181221 CreateTicketsCmd .Flags ().StringVar (& flakyTestJSONDBPath , "flaky-test-json-db-path" , "" , "Path to the flaky test JSON database (default: ~/.flaky_tes_db.json)" )
222+ CreateTicketsCmd .Flags ().StringVar (& assigneeMappingPath , "assignee-mapping" , "" , "Path to JSON file with assignee mapping (supports regex)" )
182223}
183224
184225// -------------------------------------------------------------------------------------
@@ -196,6 +237,7 @@ type FlakyTicket struct {
196237 Description string
197238 ExistingJiraKey string
198239 ExistingTicketSource string // "localdb" or "jira" (if found)
240+ Assignee string
199241}
200242
201243// rowToFlakyTicket: build a ticket from one CSV row (index assumptions: pkg=0, testName=2, flakeRate=7, logs=9).
@@ -207,7 +249,7 @@ func rowToFlakyTicket(row []string) FlakyTicket {
207249
208250 summary := fmt .Sprintf ("Fix Flaky Test: %s (%s%% flake rate)" , testName , flakeRate )
209251
210- // Parse logs (same as before)
252+ // Parse logs
211253 var logSection string
212254 if logs == "" {
213255 logSection = "(Logs not available)"
@@ -316,7 +358,7 @@ type model struct {
316358
317359 LocalDB localdb.DB // reference to our local DB
318360
319- mode string // "normal" or "promptExisting "
361+ mode string // "normal", "promptExisting", or "ticketCreated "
320362 inputValue string // user-typed input for existing ticket
321363}
322364
@@ -366,14 +408,11 @@ func updateNormalMode(m model, msg tea.KeyMsg) (tea.Model, tea.Cmd) {
366408 if t .ExistingJiraKey != "" && m .JiraClient != nil {
367409 err := jirautils .DeleteTicketInJira (m .JiraClient , t .ExistingJiraKey )
368410 if err != nil {
369- // Log error if deletion fails
370411 log .Error ().Err (err ).Msgf ("Failed to delete ticket %s" , t .ExistingJiraKey )
371412 } else {
372- // Clear ticket info on success
373413 t .ExistingJiraKey = ""
374414 t .ExistingTicketSource = ""
375415 m .tickets [m .index ] = t
376- // Update local DB (clearing stored ticket)
377416 m .LocalDB .Set (t .TestPackage , t .TestName , "" )
378417 }
379418 }
@@ -404,22 +443,15 @@ func updatePromptExisting(m model, msg tea.KeyMsg) (tea.Model, tea.Cmd) {
404443 m .inputValue = m .inputValue [:len (m .inputValue )- 1 ]
405444 }
406445 case tea .KeyEnter :
407- // store the typed string
408446 t := m .tickets [m .index ]
409447 t .ExistingJiraKey = m .inputValue
410- t .ExistingTicketSource = "localdb" // user-provided
448+ t .ExistingTicketSource = "localdb"
411449 m .tickets [m .index ] = t
412-
413- // update local DB
414450 m .LocalDB .Set (t .TestPackage , t .TestName , t .ExistingJiraKey )
415-
416- // back to normal mode
417451 m .mode = "normal"
418452 m .inputValue = ""
419- // skip from CSV if there's now a known ticket
420453 return updateSkip (m )
421454 case tea .KeyEsc :
422- // Cancel
423455 m .mode = "normal"
424456 m .inputValue = ""
425457 }
@@ -430,43 +462,39 @@ func updateConfirm(m model) (tea.Model, tea.Cmd) {
430462 i := m .index
431463 t := m .tickets [i ]
432464
433- // Attempt Jira creation if not dry-run and we have a client
465+ // Attempt Jira creation if not dry-run and we have a client.
466+ // Pass the assignee (if any) to the CreateTicketInJira function.
434467 if ! m .DryRun && m .JiraClient != nil {
435- issueKey , err := jirautils .CreateTicketInJira (m .JiraClient , t .Summary , t .Description , m .JiraProject , m .JiraIssueType )
468+ issueKey , err := jirautils .CreateTicketInJira (m .JiraClient , t .Summary , t .Description , m .JiraProject , m .JiraIssueType , t . Assignee )
436469 if err != nil {
437470 log .Error ().Err (err ).Msgf ("Failed to create Jira ticket: %s" , t .Summary )
438471 } else {
439472 log .Info ().Msgf ("Created Jira ticket: %s (summary=%q)" , issueKey , t .Summary )
440473 t .Confirmed = true
441474 t .ExistingJiraKey = issueKey
442475 t .ExistingTicketSource = "jira"
443- // store in local DB so we won't prompt again
444476 m .LocalDB .Set (t .TestPackage , t .TestName , issueKey )
445477 }
446478 } else {
447- // Dry run => mark confirmed (so we remove from CSV), but no actual creation
448479 log .Info ().Msgf ("[Dry Run] Would create Jira issue: %q" , t .Summary )
449480 t .Confirmed = true
450481 }
451482 m .tickets [i ] = t
452483 m .confirmed ++
453- // Instead of incrementing the index immediately, set mode to "ticketCreated"
454484 m .mode = "ticketCreated"
455485 return m , nil
456486}
457487
458488func updateTicketCreated (m model , msg tea.KeyMsg ) (tea.Model , tea.Cmd ) {
459489 switch msg .String () {
460490 case "n" :
461- // Advance to the next ticket and reset mode
462491 m .mode = "normal"
463492 m .index ++
464493 if m .index >= len (m .tickets ) {
465494 m .quitting = true
466495 }
467496 return m , nil
468497 case "e" :
469- // Switch to the promptExisting mode for manual ticket id update
470498 m .mode = "promptExisting"
471499 m .inputValue = ""
472500 return m , nil
@@ -490,13 +518,12 @@ func updateQuit(m model) (tea.Model, tea.Cmd) {
490518 return m , tea .Quit
491519}
492520
493- // View logic to handle your new requirements
521+ // View logic
494522func (m model ) View () string {
495523 if m .quitting || m .index >= len (m .tickets ) {
496524 return finalView (m )
497525 }
498526
499- // Sub-mode: prompt for existing ticket ID
500527 if m .mode == "promptExisting" {
501528 return fmt .Sprintf (
502529 "Enter existing Jira ticket ID for test %q:\n \n %s\n \n (Press Enter to confirm, Esc to cancel)" ,
@@ -516,21 +543,24 @@ func (m model) View() string {
516543
517544 t := m .tickets [m .index ]
518545
519- // 1) Header line
520546 var header string
521547 if t .Valid {
522548 header = headerStyle .Render (fmt .Sprintf ("Proposed Ticket #%d of %d" , m .index + 1 , len (m .tickets )))
523549 } else {
524550 header = headerStyle .Render (fmt .Sprintf ("Ticket #%d of %d (Invalid)" , m .index + 1 , len (m .tickets )))
525551 }
526552
527- // 2) Summary & Description
553+ // New: Assignee line above Summary
554+ var assigneeLine string
555+ if t .Assignee != "" {
556+ assigneeLine = summaryStyle .Render (fmt .Sprintf ("Assignee: %s" , t .Assignee ))
557+ }
558+
528559 sum := summaryStyle .Render ("Summary:" )
529560 sumBody := descBodyStyle .Render (t .Summary )
530561 descHeader := descHeaderStyle .Render ("\n Description:" )
531562 descBody := descBodyStyle .Render (t .Description )
532563
533- // 3) Existing ticket line
534564 existingLine := ""
535565 if t .ExistingJiraKey != "" {
536566 prefix := "Existing ticket found"
@@ -548,13 +578,11 @@ func (m model) View() string {
548578 existingLine = existingStyle .Render (fmt .Sprintf ("\n %s: %s" , prefix , link ))
549579 }
550580
551- // 4) If invalid: show reason
552581 invalidLine := ""
553582 if ! t .Valid {
554583 invalidLine = errorStyle .Render (fmt .Sprintf ("\n Cannot create ticket: %s" , t .InvalidReason ))
555584 }
556585
557- // 5) Help line
558586 var helpLine string
559587 if ! t .Valid {
560588 if t .ExistingJiraKey != "" {
@@ -564,7 +592,6 @@ func (m model) View() string {
564592 }
565593 } else {
566594 if t .ExistingJiraKey != "" {
567- // Show the "d" option in red when a ticket is found.
568595 helpLine = fmt .Sprintf ("\n [n] to next, [e] to update ticket id, %s to remove ticket, [q] to quit." ,
569596 redStyle .Render ("[d]" ))
570597 } else {
@@ -576,8 +603,9 @@ func (m model) View() string {
576603 }
577604 }
578605
579- return fmt .Sprintf ("%s\n %s\n %s\n %s\n %s%s%s\n %s\n " ,
606+ return fmt .Sprintf ("%s\n \n %s \n \n %s\n %s\n %s\n %s%s%s\n %s\n " ,
580607 header ,
608+ assigneeLine ,
581609 sum ,
582610 sumBody ,
583611 descHeader ,
@@ -606,39 +634,32 @@ func readFlakyTestsCSV(path string) ([][]string, error) {
606634 return nil , err
607635 }
608636 defer f .Close ()
609-
610637 r := csv .NewReader (f )
611638 return r .ReadAll ()
612639}
613640
614641func writeRemainingTicketsCSV (newPath string , m model ) error {
615- // gather confirmed row indices
616642 confirmedRows := make (map [int ]bool )
617643 for _ , t := range m .tickets {
618644 if t .Confirmed || t .ExistingJiraKey != "" {
619- // If there's an existing or newly created ticket, remove from the new CSV
620645 confirmedRows [t .RowIndex ] = true
621646 }
622647 }
623-
624648 var newRecords [][]string
625649 orig := m .originalRecords
626650 if len (orig ) > 0 {
627- newRecords = append (newRecords , orig [0 ]) // header row
651+ newRecords = append (newRecords , orig [0 ])
628652 }
629-
630653 for i := 1 ; i < len (orig ); i ++ {
631654 if ! confirmedRows [i ] {
632655 newRecords = append (newRecords , orig [i ])
633656 }
634657 }
635-
636658 f , err := os .Create (newPath )
637659 if err != nil {
638660 return err
639661 }
640662 defer f .Close ()
641-
642663 w := csv .NewWriter (f )
643664 if err := w .WriteAll (newRecords ); err != nil {
644665 return err
0 commit comments