Repository: prometheus/alertmanager (2025-07-15T14:37:55Z)

Issue:
Jira integration does not reopen issues when reopen_duration is set
**Summary**

When an alert re‑fires within the configured reopen_duration, the existing Jira issue is not reopened, even though reopen_transition is configured. The issue only gets updated while it remains in the Done status.

**Alertmanager version**

alertmanager, version 0.28.1 

Minimal configuration to reproduce
```
receivers:
  - name: jira
    jira_configs:
      - send_resolved: false
        reopen_duration: 12h   
        reopen_transition: "Back to work in progress"
```
Relevant log excerpt
```
time=2025-06-12T06:14:48.826Z level=DEBUG source=dispatch.go:530 msg=flushing component=dispatcher aggrGroup="{}/{channel=\"atlassian\",device_number=~\"^(?:.+)$\",report=\"true\",severity=~\"^(?:critical|warning|info)$\"}:{alertname=\"DU02-AN\", device_number=\"0216\"}" alerts=[DU02-AN[f3dae8b][active]]
time=2025-06-12T06:14:48.826Z level=DEBUG source=jira.go:230 msg="search for recent issues" integration=jira group_key="{}/{channel=\"atlassian\",device_number=~\"^(?:.+)$\",report=\"true\",severity=~\"^(?:critical|warning|info)$\"}:{alertname=\"DU02-AN\", device_number=\"0216\"}" jql="project=\"SPT\" and labels=\"ALERT{766e703fe63f1da246fb533c404024890bdfe90f216bba7cb55b50918ce1fce5}\" order by status ASC,resolutiondate DESC"
time=2025-06-12T06:14:49.232Z level=DEBUG source=jira.go:106 msg="updating existing issue" integration=jira group_key="{}/{channel=\"atlassian\",device_number=~\"^(?:.+)$\",report=\"true\",severity=~\"^(?:critical|warning|info)$\"}:{alertname=\"DU02-AN\", device_number=\"0216\"}" issue_key=SPT-1104
```

The generated JQL find also ticket that was already resolved more than **reopen_duration** time ago

**Code pointer**


https://github.com/prometheus/alertmanager/blob/b2099eaa2c9ebc25edb26517cb9c732738e93910/notify/jira/jira.go#L205-L214


**Why this is wrong**

The reopenDuration clause is inside the else branch that executes only when the alert is not firing. As a result it tries to reopen ticket that was already resolved several days ago, while reopen_duration is only 12h

**Expected logic**

For firing alerts with a reopen_transition, Alertmanager should:

Search for issues closed within reopen_duration.

If found, reopen them using the supplied transition.

Otherwise create a new issue.

**Reference documentation**

The Grafana Alerting docs describe Reopen Duration as: "The time duration (in minutes) to control whether to reopen an issue that was closed within this duration or create a new one." this matches the expected behaviour above.




Discussion:
@grobinson-grafana hi, if no one is working on that issue, I'd like to work on this. Could you please assign it to me?
You can just open a PR for it, it doesn't need to be assigned 👍 
Hey @grobinson-grafana , I've opened up the [PR](https://github.com/prometheus/alertmanager/pull/4478). Could you please take a look when you have some time?



Patch:
diff --git a/notify/jira/jira.go b/notify/jira/jira.go
index 259a5e62aa..1ebbaad604 100644
--- a/notify/jira/jira.go
+++ b/notify/jira/jira.go
@@ -201,16 +201,17 @@ func (n *Notifier) searchExistingIssue(ctx context.Context, logger *slog.Logger,
 		jql.WriteString(fmt.Sprintf(`resolution != %q and `, n.conf.WontFixResolution))
 	}
 
-	// If the group is firing, do not search for closed issues unless a reopen transition is defined.
+	// If the group is firing, search for open issues. If a reopen transition is
+	// defined, also search for issues that were closed within the reopen duration.
 	if firing {
-		if n.conf.ReopenTransition == "" {
-			jql.WriteString(`statusCategory != Done and `)
-		}
-	} else {
 		reopenDuration := int64(time.Duration(n.conf.ReopenDuration).Minutes())
-		if reopenDuration != 0 {
+		if n.conf.ReopenTransition != "" && reopenDuration > 0 {
 			jql.WriteString(fmt.Sprintf(`(resolutiondate is EMPTY OR resolutiondate >= -%dm) and `, reopenDuration))
+		} else {
+			jql.WriteString(`statusCategory != Done and `)
 		}
+	} else {
+		jql.WriteString(`statusCategory != Done and `)
 	}
 
 	alertLabel := fmt.Sprintf("ALERT{%s}", groupID)


Test patch:
diff --git a/notify/jira/jira_test.go b/notify/jira/jira_test.go
index 8d80647052..39b49dacfa 100644
--- a/notify/jira/jira_test.go
+++ b/notify/jira/jira_test.go
@@ -100,19 +100,45 @@ func TestSearchExistingIssue(t *testing.T) {
 		title         string
 		cfg           *config.JiraConfig
 		groupKey      string
+		firing        bool
 		expectedJQL   string
 		expectedIssue *issue
 		expectedErr   bool
 		expectedRetry bool
 	}{
 		{
-			title: "search existing issue with project template",
+			title: "search existing issue with project template for firing alert",
 			cfg: &config.JiraConfig{
 				Summary:     `{{ template "jira.default.summary" . }}`,
 				Description: `{{ template "jira.default.description" . }}`,
 				Project:     `{{ .CommonLabels.project }}`,
 			},
 			groupKey:    "1",
+			firing:      true,
+			expectedJQL: `statusCategory != Done and project="PROJ" and labels="ALERT{1}" order by status ASC,resolutiondate DESC`,
+		},
+		{
+			title: "search existing issue with reopen duration for firing alert",
+			cfg: &config.JiraConfig{
+				Summary:          `{{ template "jira.default.summary" . }}`,
+				Description:      `{{ template "jira.default.description" . }}`,
+				Project:          `{{ .CommonLabels.project }}`,
+				ReopenDuration:   model.Duration(60 * time.Minute),
+				ReopenTransition: "REOPEN",
+			},
+			groupKey:    "1",
+			firing:      true,
+			expectedJQL: `(resolutiondate is EMPTY OR resolutiondate >= -60m) and project="PROJ" and labels="ALERT{1}" order by status ASC,resolutiondate DESC`,
+		},
+		{
+			title: "search existing issue for resolved alert",
+			cfg: &config.JiraConfig{
+				Summary:     `{{ template "jira.default.summary" . }}`,
+				Description: `{{ template "jira.default.description" . }}`,
+				Project:     `{{ .CommonLabels.project }}`,
+			},
+			groupKey:    "1",
+			firing:      false,
 			expectedJQL: `statusCategory != Done and project="PROJ" and labels="ALERT{1}" order by status ASC,resolutiondate DESC`,
 		},
 	} {
@@ -147,7 +173,7 @@ func TestSearchExistingIssue(t *testing.T) {
 				return tmplText(tmpl), tmplTextErr
 			}
 
-			issue, retry, err := pd.searchExistingIssue(ctx, logger, tc.groupKey, true, tmplTextFunc)
+			issue, retry, err := pd.searchExistingIssue(ctx, logger, tc.groupKey, tc.firing, tmplTextFunc)
 			if tc.expectedErr {
 				require.Error(t, err)
 			} else {