Skip to content

Commit 963a1b0

Browse files
committed
Support assignee mapping
1 parent 8b232ac commit 963a1b0

File tree

2 files changed

+62
-40
lines changed

2 files changed

+62
-40
lines changed

tools/flakeguard/cmd/create_jira_tickets.go

Lines changed: 59 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ package cmd
33
import (
44
"context"
55
"encoding/csv"
6+
"encoding/json"
67
"fmt"
78
"os"
89
"path/filepath"
10+
"regexp"
911
"strings"
1012
"time"
1113

@@ -26,8 +28,15 @@ var (
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+
3140
var 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

458488
func 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
494522
func (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("\nDescription:")
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("\nCannot 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

614641
func 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

tools/flakeguard/jirautils/jira.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,16 @@ func GetJiraClient() (*jira.Client, error) {
3333
// CreateTicketInJira creates a new Jira ticket and returns its issue key.
3434
func CreateTicketInJira(
3535
client *jira.Client,
36-
summary, description, projectKey, issueType string,
36+
summary, description, projectKey, issueType, assigneeId string,
3737
) (string, error) {
3838
issue := &jira.Issue{
3939
Fields: &jira.IssueFields{
4040
Project: jira.Project{Key: projectKey},
41+
Assignee: &jira.User{AccountID: assigneeId},
4142
Summary: summary,
4243
Description: description,
4344
Type: jira.IssueType{Name: issueType},
44-
// Labels: []string{"flaky_test"}, TODO: enable
45+
Labels: []string{"flaky_test"},
4546
},
4647
}
4748
newIssue, resp, err := client.Issue.CreateWithContext(context.Background(), issue)

0 commit comments

Comments
 (0)