From eb81053bf5d99ab33c4b4dd31926cededab1aecb Mon Sep 17 00:00:00 2001 From: hyunuk Date: Sat, 26 Apr 2025 14:54:45 +0900 Subject: [PATCH] fix(jira): correctly handle issues with null resolution (Fixes #4295) Signed-off-by: hyunuk --- notify/jira/jira.go | 4 ++- notify/jira/jira_test.go | 57 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/notify/jira/jira.go b/notify/jira/jira.go index 259a5e62aa..54283095f4 100644 --- a/notify/jira/jira.go +++ b/notify/jira/jira.go @@ -198,7 +198,9 @@ func (n *Notifier) searchExistingIssue(ctx context.Context, logger *slog.Logger, jql := strings.Builder{} if n.conf.WontFixResolution != "" { - jql.WriteString(fmt.Sprintf(`resolution != %q and `, n.conf.WontFixResolution)) + // Ensure unresolved issues are included by adding `resolution is EMPTY` condition. + // This avoids excluding issues without a resolution when using `resolution != `. + jql.WriteString(fmt.Sprintf(`(resolution is EMPTY or resolution != %q) and `, n.conf.WontFixResolution)) } // If the group is firing, do not search for closed issues unless a reopen transition is defined. diff --git a/notify/jira/jira_test.go b/notify/jira/jira_test.go index 8d80647052..9edb1ff5a6 100644 --- a/notify/jira/jira_test.go +++ b/notify/jira/jira_test.go @@ -81,6 +81,19 @@ func TestSearchExistingIssue(t *testing.T) { return } require.Equal(t, expectedJQL, data.JQL) + + // Return different responses based on the request + if strings.Contains(data.JQL, `ALERT{2}`) { + // Unresolved issue exists (resolution is EMPTY) + w.Write([]byte(`{"total": 1, "issues": [{"key": "CRM-999", "fields": {"status": {"name": "To Do"}}}]}`)) + return + } + if strings.Contains(data.JQL, `ALERT{3}`) { + // Resolved issue exists (resolution is Done) + w.Write([]byte(`{"total": 1, "issues": [{"key": "CRM-998", "fields": {"status": {"name": "Done"}}}]}`)) + return + } + // Default: No matching issues found().Add(time.Hour), w.Write([]byte(`{"total": 0, "issues": []}`)) return default: @@ -115,6 +128,50 @@ func TestSearchExistingIssue(t *testing.T) { groupKey: "1", expectedJQL: `statusCategory != Done and project="PROJ" and labels="ALERT{1}" order by status ASC,resolutiondate DESC`, }, + { + title: "existing unresolved issue (resolution is EMPTY)", + cfg: &config.JiraConfig{ + Summary: `{{ template "jira.default.summary" . }}`, + Description: `{{ template "jira.default.description" . }}`, + Project: `{{ .CommonLabels.project }}`, + WontFixResolution: "Won't Fix", + }, + groupKey: "2", + expectedJQL: `(resolution is EMPTY or resolution != "Won't Fix") and statusCategory != Done and project="PROJ" and labels="ALERT{2}" order by status ASC,resolutiondate DESC`, + expectedIssue: &issue{ + Key: "CRM-999", + Fields: &issueFields{ + Status: &issueStatus{ + Name: "To Do", + StatusCategory: struct { + Key string `json:"key"` + }{Key: ""}, + }, + }, + }, + }, + { + title: "existing resolved issue (resolution Done)", + cfg: &config.JiraConfig{ + Summary: `{{ template "jira.default.summary" . }}`, + Description: `{{ template "jira.default.description" . }}`, + Project: `{{ .CommonLabels.project }}`, + WontFixResolution: "Won't Fix", + }, + groupKey: "3", + expectedJQL: `(resolution is EMPTY or resolution != "Won't Fix") and statusCategory != Done and project="PROJ" and labels="ALERT{3}" order by status ASC,resolutiondate DESC`, + expectedIssue: &issue{ + Key: "CRM-998", + Fields: &issueFields{ + Status: &issueStatus{ + Name: "Done", + StatusCategory: struct { + Key string `json:"key"` + }{Key: ""}, + }, + }, + }, + }, } { tc := tc t.Run(tc.title, func(t *testing.T) {