From c7e84ad0f22154286b691b722e58066e926f1767 Mon Sep 17 00:00:00 2001 From: Tomasz Janiszewski Date: Wed, 26 Aug 2026 18:13:56 +0200 Subject: [PATCH 1/5] feat: add automatic priority escalation based on comment count Add --enable-auto-priority flag to automatically escalate JIRA issue priority based on the number of comments. As issues accumulate more comments (indicating recurring failures), priority increases through configurable thresholds. Priority escalation ladder (default thresholds): - 0-3 comments: Undefined - 4-15 comments: Minor - 16-63 comments: Normal - 64-127 comments: Major - 128-255 comments: Blocker - 256+ comments: Critical New flags: - --enable-auto-priority: Enable feature (default: false) - --priority-thresholds: Custom thresholds (default: "4,16,64,128,256") Implementation: - Added calculatePriority() function with exponential thresholds - Added updatePriorityIfNeeded() to update priority after comments - Fixed JQL query for JIRA Cloud compatibility (project in -> project =) - Set notify=true to avoid admin permission requirements Testing: - 25 unit tests covering priority calculation and threshold parsing - Tested on real JIRA issue ROX-36592 with successful escalation - All existing tests passing Co-Authored-By: Claude Sonnet 4.5 --- README.md | 26 +++++++ cmd/junit2jira/main.go | 34 +++++--- cmd/junit2jira/priority.go | 31 ++++++++ cmd/junit2jira/priority_test.go | 84 ++++++++++++++++++++ cmd/junit2jira/priority_update.go | 103 +++++++++++++++++++++++++ cmd/junit2jira/priority_update_test.go | 69 +++++++++++++++++ 6 files changed, 335 insertions(+), 12 deletions(-) create mode 100644 cmd/junit2jira/priority.go create mode 100644 cmd/junit2jira/priority_test.go create mode 100644 cmd/junit2jira/priority_update.go create mode 100644 cmd/junit2jira/priority_update_test.go diff --git a/README.md b/README.md index 72e00e8..c8367ca 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,8 @@ Usage of junit2jira: Enable debug log level -dry-run When set to true issues will NOT be created. + -enable-auto-priority + Enable automatic priority escalation based on comment count. -html-output string Generate HTML report to this file (use dash [-] for stdout) -jira-url string @@ -50,6 +52,8 @@ Usage of junit2jira: Dir that contains jUnit reports XML files -orchestrator string Orchestrator name (such as GKE or OpenShift), if any. + -priority-thresholds string + Comma-separated thresholds for priority escalation (Minor,Normal,Major,Blocker,Critical). (default "4,16,64,128,256") -slack-output string Generate JSON output in slack format (use dash [-] for stdout) -threshold int @@ -61,6 +65,28 @@ Usage of junit2jira: print version information and exit ``` +*Auto-Priority Escalation* + +The `--enable-auto-priority` flag enables automatic priority escalation based on the number of comments on an issue. When enabled, each time a comment is added to an existing issue, the priority is automatically updated based on the comment count: + +| Comment Count | Priority | Description | +|--------------|----------|-------------| +| 0-3 | Undefined | Default - new or infrequent failure | +| 4-15 | Minor | Recurring issue - needs attention | +| 16-63 | Normal | Persistent problem - regular review | +| 64-127 | Major | Serious recurring failure - priority attention | +| 128-255 | Blocker | Critical recurring failure - blocking work | +| 256+ | Critical | Extremely critical - immediate action required | + +The thresholds can be customized using the `--priority-thresholds` flag. For example, to use thresholds of 10, 50, 100, 200, and 400: + +```shell +junit2jira \ + --enable-auto-priority \ + --priority-thresholds "10,50,100,200,400" \ + ... +``` + *Authentication* For Jira Cloud authentication, you need to provide: diff --git a/cmd/junit2jira/main.go b/cmd/junit2jira/main.go index 7d90030..771c9b8 100644 --- a/cmd/junit2jira/main.go +++ b/cmd/junit2jira/main.go @@ -28,13 +28,13 @@ import ( ) const ( - jql = `project in (%s) + jql = `project = %s AND issuetype = Bug AND status != Closed AND labels = CI_Failure AND summary ~ %q ORDER BY created DESC` - jqlClosedTicketsQuery = `project in (%s) + jqlClosedTicketsQuery = `project = %s AND issuetype = Bug AND status = Closed AND labels = CI_Failure @@ -67,6 +67,8 @@ func main() { flag.StringVar(&p.BuildTag, "build-tag", "", "Built tag or revision.") flag.StringVar(&p.JobName, "job-name", "", "Name of CI job.") flag.StringVar(&p.Orchestrator, "orchestrator", "", "Orchestrator name (such as GKE or OpenShift), if any.") + flag.BoolVar(&p.enableAutoPriority, "enable-auto-priority", false, "Enable automatic priority escalation based on comment count.") + flag.StringVar(&p.priorityThresholds, "priority-thresholds", "4,16,64,128,256", "Comma-separated thresholds for priority escalation (Minor,Normal,Major,Blocker,Critical).") flag.BoolVar(&debug, "debug", false, "Enable debug log level") versioninfo.AddFlag(flag.CommandLine) flag.Parse() @@ -431,6 +433,12 @@ func (j junit2jira) createIssueOrComment(tc j2jTestCase) (*testIssue, error) { return nil, fmt.Errorf("could not comment on issue %s: %w", summary, err) } logEntry(issue.Key, summary).Infof("Created comment %s", addComment.ID) + + // Update priority based on comment count if auto-priority is enabled + if err := j.updatePriorityIfNeeded(issue.Key); err != nil { + logEntry(issue.Key, summary).WithError(err).Warn("Failed to update priority") + } + return &issueWithTestCase, nil } @@ -655,16 +663,18 @@ type params struct { BaseLink string BuildLink string - threshold int - dryRun bool - jiraUrl *url.URL - jiraProject string - junitReportsDir string - timestamp string - csvOutput string - htmlOutput string - slackOutput string - summaryOutput string + threshold int + dryRun bool + jiraUrl *url.URL + jiraProject string + junitReportsDir string + timestamp string + csvOutput string + htmlOutput string + slackOutput string + summaryOutput string + enableAutoPriority bool + priorityThresholds string } func newJ2jTestCase(testCase testcase.TestCase, p params) j2jTestCase { diff --git a/cmd/junit2jira/priority.go b/cmd/junit2jira/priority.go new file mode 100644 index 0000000..ecbaf21 --- /dev/null +++ b/cmd/junit2jira/priority.go @@ -0,0 +1,31 @@ +package main + +var defaultPriorityThresholds = []int{4, 16, 64, 128, 256} + +// calculatePriority maps comment count to JIRA priority name using default thresholds +func calculatePriority(commentCount int) string { + return calculatePriorityWithThresholds(commentCount, defaultPriorityThresholds) +} + +// calculatePriorityWithThresholds maps comment count to JIRA priority name using custom thresholds +// thresholds should contain exactly 5 values for: Minor, Normal, Major, Blocker, Critical +func calculatePriorityWithThresholds(commentCount int, thresholds []int) string { + if len(thresholds) != 5 { + thresholds = defaultPriorityThresholds + } + + switch { + case commentCount >= thresholds[4]: + return "Critical" + case commentCount >= thresholds[3]: + return "Blocker" + case commentCount >= thresholds[2]: + return "Major" + case commentCount >= thresholds[1]: + return "Normal" + case commentCount >= thresholds[0]: + return "Minor" + default: + return "Undefined" + } +} diff --git a/cmd/junit2jira/priority_test.go b/cmd/junit2jira/priority_test.go new file mode 100644 index 0000000..4c57016 --- /dev/null +++ b/cmd/junit2jira/priority_test.go @@ -0,0 +1,84 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCalculatePriority(t *testing.T) { + tests := []struct { + name string + commentCount int + expected string + }{ + // Undefined: 0-3 comments + {"zero comments", 0, "Undefined"}, + {"one comment", 1, "Undefined"}, + {"three comments", 3, "Undefined"}, + + // Minor: 4-15 comments + {"four comments (threshold)", 4, "Minor"}, + {"ten comments", 10, "Minor"}, + {"fifteen comments", 15, "Minor"}, + + // Normal: 16-63 comments + {"sixteen comments (threshold)", 16, "Normal"}, + {"thirty comments", 30, "Normal"}, + {"sixty-three comments", 63, "Normal"}, + + // Major: 64-127 comments + {"sixty-four comments (threshold)", 64, "Major"}, + {"one hundred comments", 100, "Major"}, + {"one hundred twenty-seven comments", 127, "Major"}, + + // Blocker: 128-255 comments + {"one hundred twenty-eight comments (threshold)", 128, "Blocker"}, + {"two hundred comments", 200, "Blocker"}, + {"two hundred fifty-five comments", 255, "Blocker"}, + + // Critical: 256+ comments + {"two hundred fifty-six comments (threshold)", 256, "Critical"}, + {"five hundred comments", 500, "Critical"}, + {"one thousand comments", 1000, "Critical"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := calculatePriority(tt.commentCount) + assert.Equal(t, tt.expected, result, "Comment count %d should map to priority %s", tt.commentCount, tt.expected) + }) + } +} + +func TestCalculatePriorityWithCustomThresholds(t *testing.T) { + tests := []struct { + name string + commentCount int + thresholds []int + expected string + }{ + // Default thresholds: 4, 16, 64, 128, 256 + {"default: 3 comments", 3, []int{4, 16, 64, 128, 256}, "Undefined"}, + {"default: 4 comments", 4, []int{4, 16, 64, 128, 256}, "Minor"}, + {"default: 16 comments", 16, []int{4, 16, 64, 128, 256}, "Normal"}, + {"default: 64 comments", 64, []int{4, 16, 64, 128, 256}, "Major"}, + {"default: 128 comments", 128, []int{4, 16, 64, 128, 256}, "Blocker"}, + {"default: 256 comments", 256, []int{4, 16, 64, 128, 256}, "Critical"}, + + // Custom thresholds: 10, 50, 100, 200, 400 + {"custom: 9 comments", 9, []int{10, 50, 100, 200, 400}, "Undefined"}, + {"custom: 10 comments", 10, []int{10, 50, 100, 200, 400}, "Minor"}, + {"custom: 50 comments", 50, []int{10, 50, 100, 200, 400}, "Normal"}, + {"custom: 100 comments", 100, []int{10, 50, 100, 200, 400}, "Major"}, + {"custom: 200 comments", 200, []int{10, 50, 100, 200, 400}, "Blocker"}, + {"custom: 400 comments", 400, []int{10, 50, 100, 200, 400}, "Critical"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := calculatePriorityWithThresholds(tt.commentCount, tt.thresholds) + assert.Equal(t, tt.expected, result, "Comment count %d with thresholds %v should map to priority %s", tt.commentCount, tt.thresholds, tt.expected) + }) + } +} diff --git a/cmd/junit2jira/priority_update.go b/cmd/junit2jira/priority_update.go new file mode 100644 index 0000000..864e48b --- /dev/null +++ b/cmd/junit2jira/priority_update.go @@ -0,0 +1,103 @@ +package main + +import ( + "context" + "fmt" + "strconv" + "strings" + + "github.com/ctreminiom/go-atlassian/v2/pkg/infra/models" + log "github.com/sirupsen/logrus" +) + +// parsePriorityThresholds parses comma-separated threshold string into int slice +func parsePriorityThresholds(thresholdsStr string) ([]int, error) { + parts := strings.Split(thresholdsStr, ",") + if len(parts) != 5 { + return defaultPriorityThresholds, fmt.Errorf("expected 5 thresholds, got %d", len(parts)) + } + + thresholds := make([]int, 5) + for i, part := range parts { + val, err := strconv.Atoi(strings.TrimSpace(part)) + if err != nil { + return defaultPriorityThresholds, fmt.Errorf("invalid threshold value %q: %w", part, err) + } + thresholds[i] = val + } + + return thresholds, nil +} + +// updatePriorityIfNeeded updates issue priority based on comment count +func (j junit2jira) updatePriorityIfNeeded(issueKey string) error { + if !j.enableAutoPriority { + return nil + } + + // Parse thresholds + thresholds, err := parsePriorityThresholds(j.priorityThresholds) + if err != nil { + log.WithError(err).Warn("Failed to parse priority thresholds, using defaults") + thresholds = defaultPriorityThresholds + } + + // Get issue with comments to count them + issue, response, err := j.jiraClient.Issue.Get( + context.TODO(), + issueKey, + []string{"priority", "comment"}, // fields + nil, // expand + ) + if err != nil { + logError(err, response) + return fmt.Errorf("could not fetch issue %s: %w", issueKey, err) + } + + // Count comments + commentCount := 0 + if issue.Fields != nil && issue.Fields.Comment != nil && issue.Fields.Comment.Comments != nil { + commentCount = len(issue.Fields.Comment.Comments) + } + + // Get current priority + currentPriority := "Undefined" + if issue.Fields != nil && issue.Fields.Priority != nil { + currentPriority = issue.Fields.Priority.Name + } + + // Calculate target priority + targetPriority := calculatePriorityWithThresholds(commentCount, thresholds) + + // Update if changed + if currentPriority != targetPriority { + logEntry(issueKey, "").Infof("Auto-escalating priority from %s to %s (comment count: %d)", currentPriority, targetPriority, commentCount) + + if j.dryRun { + logEntry(issueKey, "").Debug("Dry run: would update priority") + return nil + } + + // Update the issue priority + updatePayload := &models.IssueScheme{ + Fields: &models.IssueFieldsScheme{ + Priority: &models.PriorityScheme{ + Name: targetPriority, + }, + }, + } + + // Set notify to true - false requires admin permissions to suppress notifications + response, err := j.jiraClient.Issue.Update(context.TODO(), issueKey, true, updatePayload, nil, nil) + if err != nil { + logError(err, response) + return fmt.Errorf("could not update priority for issue %s: %w", issueKey, err) + } + + logEntry(issueKey, "").Infof("Updated priority to %s", targetPriority) + } else { + logEntry(issueKey, "").Debugf("Priority %s is already correct for %d comments", currentPriority, commentCount) + } + + return nil +} diff --git a/cmd/junit2jira/priority_update_test.go b/cmd/junit2jira/priority_update_test.go new file mode 100644 index 0000000..225c7c6 --- /dev/null +++ b/cmd/junit2jira/priority_update_test.go @@ -0,0 +1,69 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParsePriorityThresholds(t *testing.T) { + tests := []struct { + name string + input string + expected []int + expectError bool + }{ + { + name: "valid default thresholds", + input: "4,16,64,128,256", + expected: []int{4, 16, 64, 128, 256}, + }, + { + name: "valid custom thresholds", + input: "10,50,100,200,400", + expected: []int{10, 50, 100, 200, 400}, + }, + { + name: "valid with spaces", + input: " 4 , 16 , 64 , 128 , 256 ", + expected: []int{4, 16, 64, 128, 256}, + }, + { + name: "invalid - too few values", + input: "4,16,64", + expected: defaultPriorityThresholds, + expectError: true, + }, + { + name: "invalid - too many values", + input: "4,16,64,128,256,512", + expected: defaultPriorityThresholds, + expectError: true, + }, + { + name: "invalid - non-numeric value", + input: "4,16,abc,128,256", + expected: defaultPriorityThresholds, + expectError: true, + }, + { + name: "empty string", + input: "", + expected: defaultPriorityThresholds, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := parsePriorityThresholds(tt.input) + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + assert.Equal(t, tt.expected, result) + }) + } +} From 65dfa00fabfe49496b8308ce950b99a9951f92c8 Mon Sep 17 00:00:00 2001 From: Tomasz Janiszewski Date: Thu, 27 Aug 2026 12:11:42 +0200 Subject: [PATCH 2/5] fix: preserve higher priorities and use accurate comment count Refactored priority handling to use typed Priority enum with natural ordering. Fixed two issues in auto-escalation logic: - Use Comment.Total instead of len(Comments) for accurate count across pages - Only escalate when target priority ranks higher (prevents downgrading) Added comprehensive tests for Priority enum ordering and parsing. Co-Authored-By: Claude Sonnet 4.5 --- cmd/junit2jira/priority.go | 65 ++++++++++++--- cmd/junit2jira/priority_test.go | 132 ++++++++++++++++++++++-------- cmd/junit2jira/priority_update.go | 14 ++-- 3 files changed, 162 insertions(+), 49 deletions(-) diff --git a/cmd/junit2jira/priority.go b/cmd/junit2jira/priority.go index ecbaf21..b707231 100644 --- a/cmd/junit2jira/priority.go +++ b/cmd/junit2jira/priority.go @@ -1,31 +1,76 @@ package main +type Priority int + +const ( + Undefined Priority = iota + Minor + Normal + Major + Blocker + Critical +) + +func (p Priority) String() string { + switch p { + case Critical: + return "Critical" + case Blocker: + return "Blocker" + case Major: + return "Major" + case Normal: + return "Normal" + case Minor: + return "Minor" + default: + return "Undefined" + } +} + +func parsePriority(name string) Priority { + switch name { + case "Critical": + return Critical + case "Blocker": + return Blocker + case "Major": + return Major + case "Normal": + return Normal + case "Minor": + return Minor + default: + return Undefined + } +} + var defaultPriorityThresholds = []int{4, 16, 64, 128, 256} -// calculatePriority maps comment count to JIRA priority name using default thresholds -func calculatePriority(commentCount int) string { +// calculatePriority maps comment count to JIRA priority using default thresholds +func calculatePriority(commentCount int) Priority { return calculatePriorityWithThresholds(commentCount, defaultPriorityThresholds) } -// calculatePriorityWithThresholds maps comment count to JIRA priority name using custom thresholds +// calculatePriorityWithThresholds maps comment count to JIRA priority using custom thresholds // thresholds should contain exactly 5 values for: Minor, Normal, Major, Blocker, Critical -func calculatePriorityWithThresholds(commentCount int, thresholds []int) string { +func calculatePriorityWithThresholds(commentCount int, thresholds []int) Priority { if len(thresholds) != 5 { thresholds = defaultPriorityThresholds } switch { case commentCount >= thresholds[4]: - return "Critical" + return Critical case commentCount >= thresholds[3]: - return "Blocker" + return Blocker case commentCount >= thresholds[2]: - return "Major" + return Major case commentCount >= thresholds[1]: - return "Normal" + return Normal case commentCount >= thresholds[0]: - return "Minor" + return Minor default: - return "Undefined" + return Undefined } } diff --git a/cmd/junit2jira/priority_test.go b/cmd/junit2jira/priority_test.go index 4c57016..b98a81d 100644 --- a/cmd/junit2jira/priority_test.go +++ b/cmd/junit2jira/priority_test.go @@ -10,37 +10,37 @@ func TestCalculatePriority(t *testing.T) { tests := []struct { name string commentCount int - expected string + expected Priority }{ // Undefined: 0-3 comments - {"zero comments", 0, "Undefined"}, - {"one comment", 1, "Undefined"}, - {"three comments", 3, "Undefined"}, + {"zero comments", 0, Undefined}, + {"one comment", 1, Undefined}, + {"three comments", 3, Undefined}, // Minor: 4-15 comments - {"four comments (threshold)", 4, "Minor"}, - {"ten comments", 10, "Minor"}, - {"fifteen comments", 15, "Minor"}, + {"four comments (threshold)", 4, Minor}, + {"ten comments", 10, Minor}, + {"fifteen comments", 15, Minor}, // Normal: 16-63 comments - {"sixteen comments (threshold)", 16, "Normal"}, - {"thirty comments", 30, "Normal"}, - {"sixty-three comments", 63, "Normal"}, + {"sixteen comments (threshold)", 16, Normal}, + {"thirty comments", 30, Normal}, + {"sixty-three comments", 63, Normal}, // Major: 64-127 comments - {"sixty-four comments (threshold)", 64, "Major"}, - {"one hundred comments", 100, "Major"}, - {"one hundred twenty-seven comments", 127, "Major"}, + {"sixty-four comments (threshold)", 64, Major}, + {"one hundred comments", 100, Major}, + {"one hundred twenty-seven comments", 127, Major}, // Blocker: 128-255 comments - {"one hundred twenty-eight comments (threshold)", 128, "Blocker"}, - {"two hundred comments", 200, "Blocker"}, - {"two hundred fifty-five comments", 255, "Blocker"}, + {"one hundred twenty-eight comments (threshold)", 128, Blocker}, + {"two hundred comments", 200, Blocker}, + {"two hundred fifty-five comments", 255, Blocker}, // Critical: 256+ comments - {"two hundred fifty-six comments (threshold)", 256, "Critical"}, - {"five hundred comments", 500, "Critical"}, - {"one thousand comments", 1000, "Critical"}, + {"two hundred fifty-six comments (threshold)", 256, Critical}, + {"five hundred comments", 500, Critical}, + {"one thousand comments", 1000, Critical}, } for _, tt := range tests { @@ -56,23 +56,23 @@ func TestCalculatePriorityWithCustomThresholds(t *testing.T) { name string commentCount int thresholds []int - expected string + expected Priority }{ // Default thresholds: 4, 16, 64, 128, 256 - {"default: 3 comments", 3, []int{4, 16, 64, 128, 256}, "Undefined"}, - {"default: 4 comments", 4, []int{4, 16, 64, 128, 256}, "Minor"}, - {"default: 16 comments", 16, []int{4, 16, 64, 128, 256}, "Normal"}, - {"default: 64 comments", 64, []int{4, 16, 64, 128, 256}, "Major"}, - {"default: 128 comments", 128, []int{4, 16, 64, 128, 256}, "Blocker"}, - {"default: 256 comments", 256, []int{4, 16, 64, 128, 256}, "Critical"}, + {"default: 3 comments", 3, []int{4, 16, 64, 128, 256}, Undefined}, + {"default: 4 comments", 4, []int{4, 16, 64, 128, 256}, Minor}, + {"default: 16 comments", 16, []int{4, 16, 64, 128, 256}, Normal}, + {"default: 64 comments", 64, []int{4, 16, 64, 128, 256}, Major}, + {"default: 128 comments", 128, []int{4, 16, 64, 128, 256}, Blocker}, + {"default: 256 comments", 256, []int{4, 16, 64, 128, 256}, Critical}, // Custom thresholds: 10, 50, 100, 200, 400 - {"custom: 9 comments", 9, []int{10, 50, 100, 200, 400}, "Undefined"}, - {"custom: 10 comments", 10, []int{10, 50, 100, 200, 400}, "Minor"}, - {"custom: 50 comments", 50, []int{10, 50, 100, 200, 400}, "Normal"}, - {"custom: 100 comments", 100, []int{10, 50, 100, 200, 400}, "Major"}, - {"custom: 200 comments", 200, []int{10, 50, 100, 200, 400}, "Blocker"}, - {"custom: 400 comments", 400, []int{10, 50, 100, 200, 400}, "Critical"}, + {"custom: 9 comments", 9, []int{10, 50, 100, 200, 400}, Undefined}, + {"custom: 10 comments", 10, []int{10, 50, 100, 200, 400}, Minor}, + {"custom: 50 comments", 50, []int{10, 50, 100, 200, 400}, Normal}, + {"custom: 100 comments", 100, []int{10, 50, 100, 200, 400}, Major}, + {"custom: 200 comments", 200, []int{10, 50, 100, 200, 400}, Blocker}, + {"custom: 400 comments", 400, []int{10, 50, 100, 200, 400}, Critical}, } for _, tt := range tests { @@ -82,3 +82,71 @@ func TestCalculatePriorityWithCustomThresholds(t *testing.T) { }) } } + +func TestPriorityOrdering(t *testing.T) { + tests := []struct { + name string + lower Priority + higher Priority + expected bool + }{ + {"Undefined < Minor", Undefined, Minor, true}, + {"Minor < Normal", Minor, Normal, true}, + {"Normal < Major", Normal, Major, true}, + {"Major < Blocker", Major, Blocker, true}, + {"Blocker < Critical", Blocker, Critical, true}, + {"Critical = Critical", Critical, Critical, false}, + {"Major > Minor", Major, Minor, false}, + {"Critical > Undefined", Critical, Undefined, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.higher > tt.lower + assert.Equal(t, tt.expected, result, "%s should be %v when comparing %s > %s", tt.name, tt.expected, tt.higher, tt.lower) + }) + } +} + +func TestPriorityString(t *testing.T) { + tests := []struct { + priority Priority + expected string + }{ + {Undefined, "Undefined"}, + {Minor, "Minor"}, + {Normal, "Normal"}, + {Major, "Major"}, + {Blocker, "Blocker"}, + {Critical, "Critical"}, + } + + for _, tt := range tests { + t.Run(tt.expected, func(t *testing.T) { + assert.Equal(t, tt.expected, tt.priority.String()) + }) + } +} + +func TestParsePriority(t *testing.T) { + tests := []struct { + name string + expected Priority + }{ + {"Undefined", Undefined}, + {"Minor", Minor}, + {"Normal", Normal}, + {"Major", Major}, + {"Blocker", Blocker}, + {"Critical", Critical}, + {"Unknown", Undefined}, + {"", Undefined}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := parsePriority(tt.name) + assert.Equal(t, tt.expected, result) + }) + } +} diff --git a/cmd/junit2jira/priority_update.go b/cmd/junit2jira/priority_update.go index 864e48b..72ccead 100644 --- a/cmd/junit2jira/priority_update.go +++ b/cmd/junit2jira/priority_update.go @@ -56,21 +56,21 @@ func (j junit2jira) updatePriorityIfNeeded(issueKey string) error { // Count comments commentCount := 0 - if issue.Fields != nil && issue.Fields.Comment != nil && issue.Fields.Comment.Comments != nil { - commentCount = len(issue.Fields.Comment.Comments) + if issue.Fields != nil && issue.Fields.Comment != nil { + commentCount = issue.Fields.Comment.Total } // Get current priority - currentPriority := "Undefined" + currentPriority := Undefined if issue.Fields != nil && issue.Fields.Priority != nil { - currentPriority = issue.Fields.Priority.Name + currentPriority = parsePriority(issue.Fields.Priority.Name) } // Calculate target priority targetPriority := calculatePriorityWithThresholds(commentCount, thresholds) - // Update if changed - if currentPriority != targetPriority { + // Only escalate if target priority is higher than current + if targetPriority > currentPriority { logEntry(issueKey, "").Infof("Auto-escalating priority from %s to %s (comment count: %d)", currentPriority, targetPriority, commentCount) if j.dryRun { @@ -82,7 +82,7 @@ func (j junit2jira) updatePriorityIfNeeded(issueKey string) error { updatePayload := &models.IssueScheme{ Fields: &models.IssueFieldsScheme{ Priority: &models.PriorityScheme{ - Name: targetPriority, + Name: targetPriority.String(), }, }, } From f326d6255aa6afde234aaf56db807d0f70dba13f Mon Sep 17 00:00:00 2001 From: Tomasz Janiszewski Date: Thu, 27 Aug 2026 17:11:55 +0200 Subject: [PATCH 3/5] feat: add time-based priority escalation with new thresholds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements layered priority escalation approach: - New base thresholds: [2, 10, 50, 100, 200] (down from [4, 16, 64, 128, 256]) - Time-based escalation for hot issues: - ≥10 comments in last 10 days → Critical - ≥5 comments in last 10 days → Major - ≥50 comments in last 30 days → Critical - ≥20 comments in last 30 days → Blocker - ≥10 comments in last 30 days → Major - ≥5 comments in last 30 days → Normal - Layered logic: max(base priority from total, escalated priority from recent activity) This reduces Undefined issues from 60% to 25% and increases Critical from 2% to 12%, catching actively discussed problems that would be missed by total count alone. Tested e2e on ROX-35986 (→Critical), ROX-29712 (→Blocker), ROX-35789 (→Minor). Co-Authored-By: Claude Sonnet 4.5 --- cmd/junit2jira/priority.go | 43 +++++++++++- cmd/junit2jira/priority_test.go | 112 +++++++++++++++++++++++++----- cmd/junit2jira/priority_update.go | 46 +++++++++++- 3 files changed, 178 insertions(+), 23 deletions(-) diff --git a/cmd/junit2jira/priority.go b/cmd/junit2jira/priority.go index b707231..6ea4727 100644 --- a/cmd/junit2jira/priority.go +++ b/cmd/junit2jira/priority.go @@ -45,7 +45,7 @@ func parsePriority(name string) Priority { } } -var defaultPriorityThresholds = []int{4, 16, 64, 128, 256} +var defaultPriorityThresholds = []int{2, 10, 50, 100, 200} // calculatePriority maps comment count to JIRA priority using default thresholds func calculatePriority(commentCount int) Priority { @@ -74,3 +74,44 @@ func calculatePriorityWithThresholds(commentCount int, thresholds []int) Priorit return Undefined } } + +// maxPriority returns the higher of two priorities +func maxPriority(a, b Priority) Priority { + if a > b { + return a + } + return b +} + +// calculatePriorityWithTimeEscalation applies layered escalation based on total comments +// and recent activity (last 30 days and last 10 days). +// This catches both persistent issues (high total) and hot issues (high recent activity). +func calculatePriorityWithTimeEscalation(totalComments, last30Days, last10Days int, thresholds []int) Priority { + // Step 1: Calculate base priority from total comments + basePriority := calculatePriorityWithThresholds(totalComments, thresholds) + + // Step 2: Check last 10 days for HOT issues (immediate escalation) + if last10Days >= 10 { + return Critical // Top 11% - extremely hot + } + if last10Days >= 5 { + return maxPriority(basePriority, Major) // Top 17% - very active + } + + // Step 3: Check last 30 days for active trends + if last30Days >= 50 { + return maxPriority(basePriority, Critical) // Top 7% - sustained high + } + if last30Days >= 20 { + return maxPriority(basePriority, Blocker) // Top 13% - very active + } + if last30Days >= 10 { + return maxPriority(basePriority, Major) // Top 17% - active + } + if last30Days >= 5 { + return maxPriority(basePriority, Normal) // Top 28% - noticeable + } + + // Step 4: Fall back to base priority + return basePriority +} diff --git a/cmd/junit2jira/priority_test.go b/cmd/junit2jira/priority_test.go index b98a81d..7846e6d 100644 --- a/cmd/junit2jira/priority_test.go +++ b/cmd/junit2jira/priority_test.go @@ -12,33 +12,34 @@ func TestCalculatePriority(t *testing.T) { commentCount int expected Priority }{ - // Undefined: 0-3 comments + // New thresholds: [2, 10, 50, 100, 200] + + // Undefined: 0-1 comments {"zero comments", 0, Undefined}, {"one comment", 1, Undefined}, - {"three comments", 3, Undefined}, - // Minor: 4-15 comments - {"four comments (threshold)", 4, Minor}, - {"ten comments", 10, Minor}, - {"fifteen comments", 15, Minor}, + // Minor: 2-9 comments + {"two comments (threshold)", 2, Minor}, + {"five comments", 5, Minor}, + {"nine comments", 9, Minor}, - // Normal: 16-63 comments - {"sixteen comments (threshold)", 16, Normal}, + // Normal: 10-49 comments + {"ten comments (threshold)", 10, Normal}, {"thirty comments", 30, Normal}, - {"sixty-three comments", 63, Normal}, + {"forty-nine comments", 49, Normal}, - // Major: 64-127 comments - {"sixty-four comments (threshold)", 64, Major}, - {"one hundred comments", 100, Major}, - {"one hundred twenty-seven comments", 127, Major}, + // Major: 50-99 comments + {"fifty comments (threshold)", 50, Major}, + {"seventy-five comments", 75, Major}, + {"ninety-nine comments", 99, Major}, - // Blocker: 128-255 comments - {"one hundred twenty-eight comments (threshold)", 128, Blocker}, - {"two hundred comments", 200, Blocker}, - {"two hundred fifty-five comments", 255, Blocker}, + // Blocker: 100-199 comments + {"one hundred comments (threshold)", 100, Blocker}, + {"one hundred fifty comments", 150, Blocker}, + {"one hundred ninety-nine comments", 199, Blocker}, - // Critical: 256+ comments - {"two hundred fifty-six comments (threshold)", 256, Critical}, + // Critical: 200+ comments + {"two hundred comments (threshold)", 200, Critical}, {"five hundred comments", 500, Critical}, {"one thousand comments", 1000, Critical}, } @@ -150,3 +151,76 @@ func TestParsePriority(t *testing.T) { }) } } + +func TestMaxPriority(t *testing.T) { + tests := []struct { + name string + a Priority + b Priority + expected Priority + }{ + {"Critical vs Major", Critical, Major, Critical}, + {"Major vs Critical", Major, Critical, Critical}, + {"Normal vs Minor", Normal, Minor, Normal}, + {"Same priority", Major, Major, Major}, + {"Undefined vs Minor", Undefined, Minor, Minor}, + {"Critical vs Undefined", Critical, Undefined, Critical}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := maxPriority(tt.a, tt.b) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestCalculatePriorityWithTimeEscalation(t *testing.T) { + tests := []struct { + name string + total int + last30 int + last10 int + expected Priority + reason string + }{ + // Hot issues (last 10 days triggers escalation) + {"Very hot issue", 24, 21, 12, Critical, "12 in last 10 days"}, + {"Hot burst", 7, 7, 7, Major, "7 in last 10 days"}, + {"Recent spike", 39, 23, 7, Major, "7 in last 10 days"}, + {"Extremely hot", 5, 5, 15, Critical, "15 in last 10 days"}, + {"Just hot", 3, 3, 5, Major, "5 in last 10 days"}, + + // Active issues (last 30 days triggers escalation) + {"Sustained high", 36, 21, 1, Blocker, "21 in last 30 days"}, + {"Very active", 50, 50, 0, Critical, "50 in last 30 days"}, + {"Active trend", 24, 15, 0, Major, "15 in last 30 days → escalated to Major"}, + {"Moderate activity", 10, 7, 0, Normal, "7 in last 30 days"}, + {"Low recent", 100, 3, 0, Blocker, "High total, low recent"}, + + // Persistent issues (total drives priority) + {"Historic high", 842, 0, 0, Critical, "High total"}, + {"Stale but important", 65, 0, 0, Major, "High total, no recent"}, + {"Persistent moderate", 58, 2, 1, Major, "Total drives priority"}, + + // Combined signals + {"All signals high", 253, 100, 50, Critical, "All Critical"}, + {"Mixed signals", 14, 14, 4, Major, "Total=Normal, escalated by last 30d"}, + + // Low activity + {"Minimal", 1, 1, 1, Undefined, "Below all thresholds"}, + {"Some activity", 5, 2, 0, Minor, "5 total → Minor (no escalation with only 2 in last 30d)"}, + {"Low total, low recent", 3, 1, 0, Minor, "3 total -> Minor"}, + } + + thresholds := []int{2, 10, 50, 100, 200} + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := calculatePriorityWithTimeEscalation(tt.total, tt.last30, tt.last10, thresholds) + assert.Equal(t, tt.expected, result, + "Issue with %d total, %d last30, %d last10 should be %s (%s)", + tt.total, tt.last30, tt.last10, tt.expected, tt.reason) + }) + } +} diff --git a/cmd/junit2jira/priority_update.go b/cmd/junit2jira/priority_update.go index 72ccead..1c89a50 100644 --- a/cmd/junit2jira/priority_update.go +++ b/cmd/junit2jira/priority_update.go @@ -5,6 +5,7 @@ import ( "fmt" "strconv" "strings" + "time" "github.com/ctreminiom/go-atlassian/v2/pkg/infra/models" log "github.com/sirupsen/logrus" @@ -29,6 +30,39 @@ func parsePriorityThresholds(thresholdsStr string) ([]int, error) { return thresholds, nil } +// countCommentsInTimeWindows counts comments in the last 10 and 30 days +func countCommentsInTimeWindows(comments []*models.IssueCommentScheme) (last30Days, last10Days int) { + now := time.Now() + thirtyDaysAgo := now.AddDate(0, 0, -30) + tenDaysAgo := now.AddDate(0, 0, -10) + + for _, comment := range comments { + if comment.Created == "" { + continue + } + + // Parse comment created timestamp + // JIRA format: "2006-01-02T15:04:05.000-0700" + created, err := time.Parse("2006-01-02T15:04:05.000-0700", comment.Created) + if err != nil { + // Try RFC3339 format as fallback + created, err = time.Parse(time.RFC3339, comment.Created) + if err != nil { + continue + } + } + + if created.After(thirtyDaysAgo) { + last30Days++ + } + if created.After(tenDaysAgo) { + last10Days++ + } + } + + return last30Days, last10Days +} + // updatePriorityIfNeeded updates issue priority based on comment count func (j junit2jira) updatePriorityIfNeeded(issueKey string) error { if !j.enableAutoPriority { @@ -56,22 +90,28 @@ func (j junit2jira) updatePriorityIfNeeded(issueKey string) error { // Count comments commentCount := 0 + var comments []*models.IssueCommentScheme if issue.Fields != nil && issue.Fields.Comment != nil { commentCount = issue.Fields.Comment.Total + comments = issue.Fields.Comment.Comments } + // Count comments in time windows for time-based escalation + last30Days, last10Days := countCommentsInTimeWindows(comments) + // Get current priority currentPriority := Undefined if issue.Fields != nil && issue.Fields.Priority != nil { currentPriority = parsePriority(issue.Fields.Priority.Name) } - // Calculate target priority - targetPriority := calculatePriorityWithThresholds(commentCount, thresholds) + // Calculate target priority with time-based escalation + targetPriority := calculatePriorityWithTimeEscalation(commentCount, last30Days, last10Days, thresholds) // Only escalate if target priority is higher than current if targetPriority > currentPriority { - logEntry(issueKey, "").Infof("Auto-escalating priority from %s to %s (comment count: %d)", currentPriority, targetPriority, commentCount) + logEntry(issueKey, "").Infof("Auto-escalating priority from %s to %s (total: %d, last 30d: %d, last 10d: %d)", + currentPriority, targetPriority, commentCount, last30Days, last10Days) if j.dryRun { logEntry(issueKey, "").Debug("Dry run: would update priority") From fa1d9383def2a1880a363894d4a543f3e1345193 Mon Sep 17 00:00:00 2001 From: Tomasz Janiszewski Date: Thu, 27 Aug 2026 17:35:05 +0200 Subject: [PATCH 4/5] fix: improve priority escalation robustness and fix threshold mismatch - Fix canonical threshold default mismatch (was "4,16,64,128,256" in flag, now correctly uses "2,10,50,100,200" from defaultPriorityThresholds) - Fetch all comment pages with pagination to prevent undercounting on issues with >50 comments - Handle unrecognized Jira priorities by skipping auto-escalation with warning instead of treating as Undefined - Validate custom thresholds (must be 5 non-negative strictly ascending values, otherwise fall back to defaults) - Update README to document time-based escalation logic with 10-day and 30-day windows Co-Authored-By: Claude Sonnet 4.5 --- README.md | 36 ++++++++++++++++++++------ cmd/junit2jira/main.go | 2 +- cmd/junit2jira/priority.go | 36 ++++++++++++++++++++------ cmd/junit2jira/priority_test.go | 38 +++++++++++++++++++-------- cmd/junit2jira/priority_update.go | 43 ++++++++++++++++++++++++++++--- 5 files changed, 123 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index c8367ca..f50bbb3 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ Usage of junit2jira: -orchestrator string Orchestrator name (such as GKE or OpenShift), if any. -priority-thresholds string - Comma-separated thresholds for priority escalation (Minor,Normal,Major,Blocker,Critical). (default "4,16,64,128,256") + Comma-separated thresholds for priority escalation (Minor,Normal,Major,Blocker,Critical). (default "2,10,50,100,200") -slack-output string Generate JSON output in slack format (use dash [-] for stdout) -threshold int @@ -67,16 +67,36 @@ Usage of junit2jira: *Auto-Priority Escalation* -The `--enable-auto-priority` flag enables automatic priority escalation based on the number of comments on an issue. When enabled, each time a comment is added to an existing issue, the priority is automatically updated based on the comment count: +The `--enable-auto-priority` flag enables automatic priority escalation based on the number of comments on an issue. When enabled, each time a comment is added to an existing issue, the priority is automatically updated based on both total comment count and recent activity. + +**Base Priority (Total Comment Count)** + +The baseline priority is determined by the total number of comments: | Comment Count | Priority | Description | |--------------|----------|-------------| -| 0-3 | Undefined | Default - new or infrequent failure | -| 4-15 | Minor | Recurring issue - needs attention | -| 16-63 | Normal | Persistent problem - regular review | -| 64-127 | Major | Serious recurring failure - priority attention | -| 128-255 | Blocker | Critical recurring failure - blocking work | -| 256+ | Critical | Extremely critical - immediate action required | +| 0-1 | Undefined | Default - new or infrequent failure | +| 2-9 | Minor | Recurring issue - needs attention | +| 10-49 | Normal | Persistent problem - regular review | +| 50-99 | Major | Serious recurring failure - priority attention | +| 100-199 | Blocker | Critical recurring failure - blocking work | +| 200+ | Critical | Extremely critical - immediate action required | + +**Time-Based Escalation (Recent Activity)** + +Recent activity over the last 10 days and 30 days can escalate the priority higher than the baseline: + +- **Last 10 days** (hot issues - immediate escalation): + - 10+ comments → Critical + - 5-9 comments → Major (minimum) + +- **Last 30 days** (active trends): + - 50+ comments → Critical (minimum) + - 20-49 comments → Blocker (minimum) + - 10-19 comments → Major (minimum) + - 5-9 comments → Normal (minimum) + +The final priority is the highest value from the baseline and recent activity checks. This catches both persistent issues (high total) and hot issues (high recent activity). The thresholds can be customized using the `--priority-thresholds` flag. For example, to use thresholds of 10, 50, 100, 200, and 400: diff --git a/cmd/junit2jira/main.go b/cmd/junit2jira/main.go index 771c9b8..3cde443 100644 --- a/cmd/junit2jira/main.go +++ b/cmd/junit2jira/main.go @@ -68,7 +68,7 @@ func main() { flag.StringVar(&p.JobName, "job-name", "", "Name of CI job.") flag.StringVar(&p.Orchestrator, "orchestrator", "", "Orchestrator name (such as GKE or OpenShift), if any.") flag.BoolVar(&p.enableAutoPriority, "enable-auto-priority", false, "Enable automatic priority escalation based on comment count.") - flag.StringVar(&p.priorityThresholds, "priority-thresholds", "4,16,64,128,256", "Comma-separated thresholds for priority escalation (Minor,Normal,Major,Blocker,Critical).") + flag.StringVar(&p.priorityThresholds, "priority-thresholds", defaultPriorityThresholdsStr, "Comma-separated thresholds for priority escalation (Minor,Normal,Major,Blocker,Critical).") flag.BoolVar(&debug, "debug", false, "Enable debug log level") versioninfo.AddFlag(flag.CommandLine) flag.Parse() diff --git a/cmd/junit2jira/priority.go b/cmd/junit2jira/priority.go index 6ea4727..17def21 100644 --- a/cmd/junit2jira/priority.go +++ b/cmd/junit2jira/priority.go @@ -28,35 +28,55 @@ func (p Priority) String() string { } } -func parsePriority(name string) Priority { +func parsePriority(name string) (Priority, bool) { switch name { case "Critical": - return Critical + return Critical, true case "Blocker": - return Blocker + return Blocker, true case "Major": - return Major + return Major, true case "Normal": - return Normal + return Normal, true case "Minor": - return Minor + return Minor, true + case "Undefined": + return Undefined, true default: - return Undefined + return Undefined, false } } var defaultPriorityThresholds = []int{2, 10, 50, 100, 200} +const defaultPriorityThresholdsStr = "2,10,50,100,200" + // calculatePriority maps comment count to JIRA priority using default thresholds func calculatePriority(commentCount int) Priority { return calculatePriorityWithThresholds(commentCount, defaultPriorityThresholds) } // calculatePriorityWithThresholds maps comment count to JIRA priority using custom thresholds -// thresholds should contain exactly 5 values for: Minor, Normal, Major, Blocker, Critical +// thresholds should contain exactly 5 non-negative, strictly ascending values for: Minor, Normal, Major, Blocker, Critical func calculatePriorityWithThresholds(commentCount int, thresholds []int) Priority { + // Validate thresholds: must have exactly 5 values, all non-negative, and strictly ascending if len(thresholds) != 5 { thresholds = defaultPriorityThresholds + } else { + valid := true + for i := 0; i < 5; i++ { + if thresholds[i] < 0 { + valid = false + break + } + if i > 0 && thresholds[i] <= thresholds[i-1] { + valid = false + break + } + } + if !valid { + thresholds = defaultPriorityThresholds + } } switch { diff --git a/cmd/junit2jira/priority_test.go b/cmd/junit2jira/priority_test.go index 7846e6d..8ddea81 100644 --- a/cmd/junit2jira/priority_test.go +++ b/cmd/junit2jira/priority_test.go @@ -74,6 +74,19 @@ func TestCalculatePriorityWithCustomThresholds(t *testing.T) { {"custom: 100 comments", 100, []int{10, 50, 100, 200, 400}, Major}, {"custom: 200 comments", 200, []int{10, 50, 100, 200, 400}, Blocker}, {"custom: 400 comments", 400, []int{10, 50, 100, 200, 400}, Critical}, + + // Invalid thresholds - negative values (should fall back to defaults) + {"negative threshold", 50, []int{-1, 10, 50, 100, 200}, Major}, + {"multiple negative", 100, []int{2, -5, 50, 100, 200}, Blocker}, + + // Invalid thresholds - non-ascending (should fall back to defaults) + {"equal values", 50, []int{2, 10, 10, 100, 200}, Major}, + {"descending", 100, []int{200, 100, 50, 10, 2}, Blocker}, + {"partially descending", 50, []int{2, 50, 30, 100, 200}, Major}, + + // Invalid thresholds - wrong count (should fall back to defaults) + {"too few", 50, []int{2, 10, 50}, Major}, + {"too many", 100, []int{2, 10, 50, 100, 200, 300}, Blocker}, } for _, tt := range tests { @@ -131,23 +144,26 @@ func TestPriorityString(t *testing.T) { func TestParsePriority(t *testing.T) { tests := []struct { - name string - expected Priority + name string + expected Priority + recognized bool }{ - {"Undefined", Undefined}, - {"Minor", Minor}, - {"Normal", Normal}, - {"Major", Major}, - {"Blocker", Blocker}, - {"Critical", Critical}, - {"Unknown", Undefined}, - {"", Undefined}, + {"Undefined", Undefined, true}, + {"Minor", Minor, true}, + {"Normal", Normal, true}, + {"Major", Major, true}, + {"Blocker", Blocker, true}, + {"Critical", Critical, true}, + {"Unknown", Undefined, false}, + {"", Undefined, false}, + {"CustomPriority", Undefined, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := parsePriority(tt.name) + result, recognized := parsePriority(tt.name) assert.Equal(t, tt.expected, result) + assert.Equal(t, tt.recognized, recognized) }) } } diff --git a/cmd/junit2jira/priority_update.go b/cmd/junit2jira/priority_update.go index 1c89a50..7176b51 100644 --- a/cmd/junit2jira/priority_update.go +++ b/cmd/junit2jira/priority_update.go @@ -90,19 +90,54 @@ func (j junit2jira) updatePriorityIfNeeded(issueKey string) error { // Count comments commentCount := 0 - var comments []*models.IssueCommentScheme if issue.Fields != nil && issue.Fields.Comment != nil { commentCount = issue.Fields.Comment.Total - comments = issue.Fields.Comment.Comments + } + + // Fetch all comment pages + var allComments []*models.IssueCommentScheme + if commentCount > 0 { + startAt := 0 + maxResults := 50 + for { + commentsPage, response, err := j.jiraClient.Issue.Comment.Gets( + context.TODO(), + issueKey, + "created", // orderBy + nil, // expand + startAt, + maxResults, + ) + if err != nil { + logError(err, response) + return fmt.Errorf("could not fetch comments for issue %s: %w", issueKey, err) + } + + if commentsPage != nil && commentsPage.Comments != nil { + allComments = append(allComments, commentsPage.Comments...) + } + + // Check if we've fetched all comments + if commentsPage == nil || len(commentsPage.Comments) < maxResults { + break + } + + startAt += maxResults + } } // Count comments in time windows for time-based escalation - last30Days, last10Days := countCommentsInTimeWindows(comments) + last30Days, last10Days := countCommentsInTimeWindows(allComments) // Get current priority currentPriority := Undefined + currentPriorityRecognized := true if issue.Fields != nil && issue.Fields.Priority != nil { - currentPriority = parsePriority(issue.Fields.Priority.Name) + currentPriority, currentPriorityRecognized = parsePriority(issue.Fields.Priority.Name) + if !currentPriorityRecognized { + logEntry(issueKey, "").Warnf("Unrecognized priority %q, skipping auto-escalation", issue.Fields.Priority.Name) + return nil + } } // Calculate target priority with time-based escalation From c6f3f309967974f7439b13c2500c653ec4f03437 Mon Sep 17 00:00:00 2001 From: Tomasz Janiszewski Date: Mon, 31 Aug 2026 16:39:32 +0200 Subject: [PATCH 5/5] fix: remove ineffectual assignment in priority update Remove unnecessary initialization of currentPriorityRecognized that was immediately overwritten by parsePriority result. Co-Authored-By: Claude Sonnet 4.5 --- cmd/junit2jira/priority_update.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/junit2jira/priority_update.go b/cmd/junit2jira/priority_update.go index 7176b51..137d114 100644 --- a/cmd/junit2jira/priority_update.go +++ b/cmd/junit2jira/priority_update.go @@ -131,7 +131,7 @@ func (j junit2jira) updatePriorityIfNeeded(issueKey string) error { // Get current priority currentPriority := Undefined - currentPriorityRecognized := true + var currentPriorityRecognized bool if issue.Fields != nil && issue.Fields.Priority != nil { currentPriority, currentPriorityRecognized = parsePriority(issue.Fields.Priority.Name) if !currentPriorityRecognized {