From a90da796bd313df616136dcf8fa73ab4c8b5bc65 Mon Sep 17 00:00:00 2001 From: Holger Waschke <85643002+holger-waschke@users.noreply.github.com> Date: Tue, 18 Nov 2025 18:12:39 +0100 Subject: [PATCH] Jira Integration: Allow configuring issue update via parameter (#4621) * Jira Integration: Allow configuring issue update via parameter --------- Signed-off-by: Holger Waschke --- config/notifiers.go | 53 +++++++++--- docs/configuration.md | 20 ++++- notify/jira/jira.go | 34 +++++--- notify/jira/jira_test.go | 179 ++++++++++++++++++++++++++++++--------- notify/jira/types.go | 14 +-- 5 files changed, 229 insertions(+), 71 deletions(-) diff --git a/config/notifiers.go b/config/notifiers.go index 58cf8a3c2..dba9a9baa 100644 --- a/config/notifiers.go +++ b/config/notifiers.go @@ -206,10 +206,14 @@ var ( NotifierConfig: NotifierConfig{ VSendResolved: true, }, - APIType: "auto", - Summary: `{{ template "jira.default.summary" . }}`, - Description: `{{ template "jira.default.description" . }}`, - Priority: `{{ template "jira.default.priority" . }}`, + APIType: "auto", + Summary: JiraFieldConfig{ + Template: `{{ template "jira.default.summary" . }}`, + }, + Description: JiraFieldConfig{ + Template: `{{ template "jira.default.description" . }}`, + }, + Priority: `{{ template "jira.default.priority" . }}`, } DefaultMattermostConfig = MattermostConfig{ @@ -969,6 +973,13 @@ func (c *MSTeamsV2Config) UnmarshalYAML(unmarshal func(any) error) error { return nil } +type JiraFieldConfig struct { + // Template is the template string used to render the field. + Template string `yaml:"template,omitempty" json:"template,omitempty"` + // EnableUpdate indicates whether this field should be omitted when updating an existing issue. + EnableUpdate *bool `yaml:"enable_update,omitempty" json:"enable_update,omitempty"` +} + type JiraConfig struct { NotifierConfig `yaml:",inline" json:",inline"` HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` @@ -976,12 +987,12 @@ type JiraConfig struct { APIURL *URL `yaml:"api_url,omitempty" json:"api_url,omitempty"` APIType string `yaml:"api_type,omitempty" json:"api_type,omitempty"` - Project string `yaml:"project,omitempty" json:"project,omitempty"` - Summary string `yaml:"summary,omitempty" json:"summary,omitempty"` - Description string `yaml:"description,omitempty" json:"description,omitempty"` - Labels []string `yaml:"labels,omitempty" json:"labels,omitempty"` - Priority string `yaml:"priority,omitempty" json:"priority,omitempty"` - IssueType string `yaml:"issue_type,omitempty" json:"issue_type,omitempty"` + Project string `yaml:"project,omitempty" json:"project,omitempty"` + Summary JiraFieldConfig `yaml:"summary,omitempty" json:"summary,omitempty"` + Description JiraFieldConfig `yaml:"description,omitempty" json:"description,omitempty"` + Labels []string `yaml:"labels,omitempty" json:"labels,omitempty"` + Priority string `yaml:"priority,omitempty" json:"priority,omitempty"` + IssueType string `yaml:"issue_type,omitempty" json:"issue_type,omitempty"` ReopenTransition string `yaml:"reopen_transition,omitempty" json:"reopen_transition,omitempty"` ResolveTransition string `yaml:"resolve_transition,omitempty" json:"resolve_transition,omitempty"` @@ -991,6 +1002,28 @@ type JiraConfig struct { Fields map[string]any `yaml:"fields,omitempty" json:"custom_fields,omitempty"` } +func (f *JiraFieldConfig) EnableUpdateValue() bool { + if f.EnableUpdate == nil { + return true + } + return *f.EnableUpdate +} + +// Supports both the legacy string and the new object form. +func (f *JiraFieldConfig) UnmarshalYAML(unmarshal func(any) error) error { + // Try simple string first (backward compatibility). + var s string + if err := unmarshal(&s); err == nil { + f.Template = s + // DisableUpdate stays false by default. + return nil + } + + // Fallback to full object form. + type plain JiraFieldConfig + return unmarshal((*plain)(f)) +} + func (c *JiraConfig) UnmarshalYAML(unmarshal func(any) error) error { *c = DefaultJiraConfig type plain JiraConfig diff --git a/docs/configuration.md b/docs/configuration.md index 8d3c29ddc..d08a8e655 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1159,11 +1159,23 @@ The default `jira.default.description` template only works with V2. # The project key where issues are created. project: -# Issue summary template. -[ summary: | default = '{{ template "jira.default.summary" . }}' ] +# Issue summary configuration. +[ summary: + # Template for the issue summary. + [ template: | default = '{{ template "jira.default.summary" . }}' ] + + # If true, the summary will not be updated when updating an existing issue. + [ enable_update: | default = false ] +] -# Issue description template. -[ description: | default = '{{ template "jira.default.description" . }}' ] +# Issue description configuration. +[ description: + # Template for the issue description. + [ template: | default = '{{ template "jira.default.description" . }}' ] + + # If true, the description will not be updated when updating an existing issue. + [ enable_update: | default = false ] +] # Labels to be added to the issue. labels: diff --git a/notify/jira/jira.go b/notify/jira/jira.go index 08897156b..e2ecd9dac 100644 --- a/notify/jira/jira.go +++ b/notify/jira/jira.go @@ -103,8 +103,7 @@ func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) } else { path = "issue/" + existingIssue.Key method = http.MethodPut - - logger.Debug("updating existing issue", "issue_key", existingIssue.Key) + logger.Debug("updating existing issue", "issue_key", existingIssue.Key, "summary_update_enabled", n.conf.Summary.EnableUpdateValue(), "description_update_enabled", n.conf.Description.EnableUpdateValue()) } requestBody, err := n.prepareIssueRequestBody(ctx, logger, key.Hash(), tmplTextFunc) @@ -112,6 +111,15 @@ func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) return false, err } + if method == http.MethodPut && requestBody.Fields != nil { + if !n.conf.Description.EnableUpdateValue() { + requestBody.Fields.Description = nil + } + if !n.conf.Summary.EnableUpdateValue() { + requestBody.Fields.Summary = nil + } + } + _, shouldRetry, err = n.doAPIRequest(ctx, method, path, requestBody) if err != nil { return shouldRetry, fmt.Errorf("failed to %s request to %q: %w", method, path, err) @@ -121,10 +129,11 @@ func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) } func (n *Notifier) prepareIssueRequestBody(_ context.Context, logger *slog.Logger, groupID string, tmplTextFunc template.TemplateFunc) (issue, error) { - summary, err := tmplTextFunc(n.conf.Summary) + summary, err := tmplTextFunc(n.conf.Summary.Template) if err != nil { return issue{}, fmt.Errorf("summary template: %w", err) } + project, err := tmplTextFunc(n.conf.Project) if err != nil { return issue{}, fmt.Errorf("project template: %w", err) @@ -156,12 +165,12 @@ func (n *Notifier) prepareIssueRequestBody(_ context.Context, logger *slog.Logge requestBody := issue{Fields: &issueFields{ Project: &issueProject{Key: project}, Issuetype: &idNameValue{Name: issueType}, - Summary: summary, + Summary: &summary, Labels: make([]string, 0, len(n.conf.Labels)+1), Fields: fieldsWithStringKeys, }} - issueDescriptionString, err := tmplTextFunc(n.conf.Description) + issueDescriptionString, err := tmplTextFunc(n.conf.Description.Template) if err != nil { return issue{}, fmt.Errorf("description template: %w", err) } @@ -171,14 +180,13 @@ func (n *Notifier) prepareIssueRequestBody(_ context.Context, logger *slog.Logge logger.Warn("Truncated description", "max_runes", maxDescriptionLenRunes) } - requestBody.Fields.Description = issueDescriptionString - if strings.HasSuffix(n.conf.APIURL.Path, "/3") { - var issueDescription any - if err := json.Unmarshal([]byte(issueDescriptionString), &issueDescription); err != nil { - return issue{}, fmt.Errorf("description unmarshaling: %w", err) + descriptionCopy := issueDescriptionString + if isAPIv3Path(n.conf.APIURL.Path) { + if !json.Valid([]byte(descriptionCopy)) { + return issue{}, fmt.Errorf("description template: invalid JSON for API v3") } - requestBody.Fields.Description = issueDescription } + requestBody.Fields.Description = &descriptionCopy for i, label := range n.conf.Labels { label, err = tmplTextFunc(label) @@ -395,3 +403,7 @@ func (n *Notifier) doAPIRequestFullPath(ctx context.Context, method, path string return responseBody, false, nil } + +func isAPIv3Path(path string) bool { + return strings.HasSuffix(strings.TrimRight(path, "/"), "/3") +} diff --git a/notify/jira/jira_test.go b/notify/jira/jira_test.go index 3613edf4b..ac1b308eb 100644 --- a/notify/jira/jira_test.go +++ b/notify/jira/jira_test.go @@ -36,6 +36,14 @@ import ( "github.com/prometheus/alertmanager/types" ) +func stringPtr(v string) *string { + return &v +} + +func boolPtr(v bool) *bool { + return &v +} + func TestJiraRetry(t *testing.T) { notifier, err := New( &config.JiraConfig{ @@ -109,8 +117,8 @@ func TestSearchExistingIssue(t *testing.T) { { title: "search existing issue with project template for firing alert", cfg: &config.JiraConfig{ - Summary: `{{ template "jira.default.summary" . }}`, - Description: `{{ template "jira.default.description" . }}`, + Summary: config.JiraFieldConfig{Template: `{{ template "jira.default.summary" . }}`}, + Description: config.JiraFieldConfig{Template: `{{ template "jira.default.description" . }}`}, Project: `{{ .CommonLabels.project }}`, }, groupKey: "1", @@ -120,8 +128,8 @@ func TestSearchExistingIssue(t *testing.T) { { title: "search existing issue with reopen duration for firing alert", cfg: &config.JiraConfig{ - Summary: `{{ template "jira.default.summary" . }}`, - Description: `{{ template "jira.default.description" . }}`, + Summary: config.JiraFieldConfig{Template: `{{ template "jira.default.summary" . }}`}, + Description: config.JiraFieldConfig{Template: `{{ template "jira.default.description" . }}`}, Project: `{{ .CommonLabels.project }}`, ReopenDuration: model.Duration(60 * time.Minute), ReopenTransition: "REOPEN", @@ -133,8 +141,8 @@ func TestSearchExistingIssue(t *testing.T) { { title: "search existing issue for resolved alert", cfg: &config.JiraConfig{ - Summary: `{{ template "jira.default.summary" . }}`, - Description: `{{ template "jira.default.description" . }}`, + Summary: config.JiraFieldConfig{Template: `{{ template "jira.default.summary" . }}`}, + Description: config.JiraFieldConfig{Template: `{{ template "jira.default.description" . }}`}, Project: `{{ .CommonLabels.project }}`, }, groupKey: "1", @@ -347,8 +355,8 @@ func TestJiraTemplating(t *testing.T) { { title: "full-blown message with templated custom field", cfg: &config.JiraConfig{ - Summary: `{{ template "jira.default.summary" . }}`, - Description: `{{ template "jira.default.description" . }}`, + Summary: config.JiraFieldConfig{Template: `{{ template "jira.default.summary" . }}`}, + Description: config.JiraFieldConfig{Template: `{{ template "jira.default.description" . }}`}, Fields: map[string]any{ "customfield_14400": `{{ template "jira.host" . }}`, }, @@ -361,8 +369,8 @@ func TestJiraTemplating(t *testing.T) { title: "template project", cfg: &config.JiraConfig{ Project: `{{ .CommonLabels.lbl1 }}`, - Summary: `{{ template "jira.default.summary" . }}`, - Description: `{{ template "jira.default.description" . }}`, + Summary: config.JiraFieldConfig{Template: `{{ template "jira.default.summary" . }}`}, + Description: config.JiraFieldConfig{Template: `{{ template "jira.default.description" . }}`}, }, retry: false, }, @@ -370,31 +378,31 @@ func TestJiraTemplating(t *testing.T) { title: "template issue type", cfg: &config.JiraConfig{ IssueType: `{{ .CommonLabels.lbl1 }}`, - Summary: `{{ template "jira.default.summary" . }}`, - Description: `{{ template "jira.default.description" . }}`, + Summary: config.JiraFieldConfig{Template: `{{ template "jira.default.summary" . }}`}, + Description: config.JiraFieldConfig{Template: `{{ template "jira.default.description" . }}`}, }, retry: false, }, { title: "summary with templating errors", cfg: &config.JiraConfig{ - Summary: "{{ ", + Summary: config.JiraFieldConfig{Template: "{{ "}, }, errMsg: "template: :1: unclosed action", }, { title: "description with templating errors", cfg: &config.JiraConfig{ - Summary: `{{ template "jira.default.summary" . }}`, - Description: "{{ ", + Summary: config.JiraFieldConfig{Template: `{{ template "jira.default.summary" . }}`}, + Description: config.JiraFieldConfig{Template: "{{ "}, }, errMsg: "template: :1: unclosed action", }, { title: "priority with templating errors", cfg: &config.JiraConfig{ - Summary: `{{ template "jira.default.summary" . }}`, - Description: `{{ template "jira.default.description" . }}`, + Summary: config.JiraFieldConfig{Template: `{{ template "jira.default.summary" . }}`}, + Description: config.JiraFieldConfig{Template: `{{ template "jira.default.description" . }}`}, Priority: "{{ ", }, errMsg: "template: :1: unclosed action", @@ -467,8 +475,8 @@ func TestJiraNotify(t *testing.T) { { title: "create new issue", cfg: &config.JiraConfig{ - Summary: `{{ template "jira.default.summary" . }}`, - Description: `{{ template "jira.default.description" . }}`, + Summary: config.JiraFieldConfig{Template: `{{ template "jira.default.summary" . }}`}, + Description: config.JiraFieldConfig{Template: `{{ template "jira.default.description" . }}`}, IssueType: "Incident", Project: "OPS", Priority: `{{ template "jira.default.priority" . }}`, @@ -495,8 +503,8 @@ func TestJiraNotify(t *testing.T) { issue: issue{ Key: "", Fields: &issueFields{ - Summary: "[FIRING:1] test (vm1 critical)", - Description: "\n\n# Alerts Firing:\n\nLabels:\n - alertname = test\n - instance = vm1\n - severity = critical\n\nAnnotations:\n\nSource: \n\n\n\n\n", + Summary: stringPtr("[FIRING:1] test (vm1 critical)"), + Description: stringPtr("\n\n# Alerts Firing:\n\nLabels:\n - alertname = test\n - instance = vm1\n - severity = critical\n\nAnnotations:\n\nSource: \n\n\n\n\n"), Issuetype: &idNameValue{Name: "Incident"}, Labels: []string{"ALERT{6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b}", "alertmanager", "test"}, Project: &issueProject{Key: "OPS"}, @@ -506,11 +514,82 @@ func TestJiraNotify(t *testing.T) { customFieldAssetFn: func(t *testing.T, issue map[string]any) {}, errMsg: "", }, + { + title: "update existing issue with disabled summary and description", + cfg: &config.JiraConfig{ + Summary: config.JiraFieldConfig{ + Template: `{{ template "jira.default.summary" . }}`, + EnableUpdate: boolPtr(false), + }, + Description: config.JiraFieldConfig{ + Template: `{{ template "jira.default.description" . }}`, + EnableUpdate: boolPtr(false), + }, + IssueType: "{{ .CommonLabels.issue_type }}", + Project: "{{ .CommonLabels.project }}", + Priority: `{{ template "jira.default.priority" . }}`, + Labels: []string{"alertmanager", "{{ .GroupLabels.alertname }}"}, + ReopenDuration: model.Duration(1 * time.Hour), + ReopenTransition: "REOPEN", + ResolveTransition: "CLOSE", + WontFixResolution: "WONTFIX", + }, + alert: &types.Alert{ + Alert: model.Alert{ + Labels: model.LabelSet{ + "alertname": "test", + "instance": "vm1", + "severity": "critical", + "project": "MONITORING", + "issue_type": "MINOR", + }, + StartsAt: time.Now(), + EndsAt: time.Now().Add(time.Hour), + }, + }, + searchResponse: issueSearchResult{ + Issues: []issue{ + { + Key: "MONITORING-1", + Fields: &issueFields{ + Summary: stringPtr("Original Summary"), + Description: stringPtr("Original Description"), + Status: &issueStatus{ + Name: "Open", + StatusCategory: struct { + Key string `json:"key"` + }{ + Key: "open", + }, + }, + }, + }, + }, + }, + issue: issue{ + Key: "MONITORING-1", + Fields: &issueFields{ + // Summary and Description should NOT be present in the update request + Issuetype: &idNameValue{Name: "MINOR"}, + Labels: []string{"ALERT{6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b}", "alertmanager", "test"}, + Project: &issueProject{Key: "MONITORING"}, + Priority: &idNameValue{Name: "High"}, + }, + }, + customFieldAssetFn: func(t *testing.T, issue map[string]any) { + // Verify that summary and description are NOT in the update request + _, hasSummary := issue["summary"] + _, hasDescription := issue["description"] + require.False(t, hasSummary, "summary should not be present in update request") + require.False(t, hasDescription, "description should not be present in update request") + }, + errMsg: "", + }, { title: "create new issue with template project and issue type", cfg: &config.JiraConfig{ - Summary: `{{ template "jira.default.summary" . }}`, - Description: `{{ template "jira.default.description" . }}`, + Summary: config.JiraFieldConfig{Template: `{{ template "jira.default.summary" . }}`}, + Description: config.JiraFieldConfig{Template: `{{ template "jira.default.description" . }}`}, IssueType: "{{ .CommonLabels.issue_type }}", Project: "{{ .CommonLabels.project }}", Priority: `{{ template "jira.default.priority" . }}`, @@ -539,8 +618,8 @@ func TestJiraNotify(t *testing.T) { issue: issue{ Key: "", Fields: &issueFields{ - Summary: "[FIRING:1] test (vm1 MINOR MONITORING critical)", - Description: "\n\n# Alerts Firing:\n\nLabels:\n - alertname = test\n - instance = vm1\n - issue_type = MINOR\n - project = MONITORING\n - severity = critical\n\nAnnotations:\n\nSource: \n\n\n\n\n", + Summary: stringPtr("[FIRING:1] test (vm1 MINOR MONITORING critical)"), + Description: stringPtr("\n\n# Alerts Firing:\n\nLabels:\n - alertname = test\n - instance = vm1\n - issue_type = MINOR\n - project = MONITORING\n - severity = critical\n\nAnnotations:\n\nSource: \n\n\n\n\n"), Issuetype: &idNameValue{Name: "MINOR"}, Labels: []string{"ALERT{6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b}", "alertmanager", "test"}, Project: &issueProject{Key: "MONITORING"}, @@ -553,8 +632,8 @@ func TestJiraNotify(t *testing.T) { { title: "create new issue with custom field and too long summary", cfg: &config.JiraConfig{ - Summary: strings.Repeat("A", maxSummaryLenRunes+10), - Description: `{{ template "jira.default.description" . }}`, + Summary: config.JiraFieldConfig{Template: strings.Repeat("A", maxSummaryLenRunes+10)}, + Description: config.JiraFieldConfig{Template: `{{ template "jira.default.description" . }}`}, IssueType: "Incident", Project: "OPS", Priority: `{{ template "jira.default.priority" . }}`, @@ -591,8 +670,8 @@ func TestJiraNotify(t *testing.T) { issue: issue{ Key: "", Fields: &issueFields{ - Summary: strings.Repeat("A", maxSummaryLenRunes-1) + "…", - Description: "\n\n# Alerts Firing:\n\nLabels:\n - alertname = test\n - instance = vm1\n\nAnnotations:\n\nSource: \n\n\n\n\n", + Summary: stringPtr(strings.Repeat("A", maxSummaryLenRunes-1) + "…"), + Description: stringPtr("\n\n# Alerts Firing:\n\nLabels:\n - alertname = test\n - instance = vm1\n\nAnnotations:\n\nSource: \n\n\n\n\n"), Issuetype: &idNameValue{Name: "Incident"}, Labels: []string{"ALERT{6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b}", "alertmanager", "test"}, Project: &issueProject{Key: "OPS"}, @@ -613,8 +692,8 @@ func TestJiraNotify(t *testing.T) { { title: "reopen issue", cfg: &config.JiraConfig{ - Summary: `{{ template "jira.default.summary" . }}`, - Description: `{{ template "jira.default.description" . }}`, + Summary: config.JiraFieldConfig{Template: `{{ template "jira.default.summary" . }}`}, + Description: config.JiraFieldConfig{Template: `{{ template "jira.default.description" . }}`}, IssueType: "Incident", Project: "OPS", Priority: `{{ template "jira.default.priority" . }}`, @@ -654,8 +733,8 @@ func TestJiraNotify(t *testing.T) { issue: issue{ Key: "", Fields: &issueFields{ - Summary: "[FIRING:1] test (vm1)", - Description: "\n\n# Alerts Firing:\n\nLabels:\n - alertname = test\n - instance = vm1\n\nAnnotations:\n\nSource: \n\n\n\n\n", + Summary: stringPtr("[FIRING:1] test (vm1)"), + Description: stringPtr("\n\n# Alerts Firing:\n\nLabels:\n - alertname = test\n - instance = vm1\n\nAnnotations:\n\nSource: \n\n\n\n\n"), Issuetype: &idNameValue{Name: "Incident"}, Labels: []string{"ALERT{6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b}", "alertmanager", "test"}, Project: &issueProject{Key: "OPS"}, @@ -668,8 +747,8 @@ func TestJiraNotify(t *testing.T) { { title: "error resolve transition not found", cfg: &config.JiraConfig{ - Summary: `{{ template "jira.default.summary" . }}`, - Description: `{{ template "jira.default.description" . }}`, + Summary: config.JiraFieldConfig{Template: `{{ template "jira.default.summary" . }}`}, + Description: config.JiraFieldConfig{Template: `{{ template "jira.default.description" . }}`}, IssueType: "Incident", Project: "OPS", Priority: `{{ template "jira.default.priority" . }}`, @@ -709,8 +788,8 @@ func TestJiraNotify(t *testing.T) { issue: issue{ Key: "", Fields: &issueFields{ - Summary: "[RESOLVED] test (vm1)", - Description: "\n\n\n# Alerts Resolved:\n\nLabels:\n - alertname = test\n - instance = vm1\n\nAnnotations:\n\nSource: \n\n\n\n", + Summary: stringPtr("[RESOLVED] test (vm1)"), + Description: stringPtr("\n\n\n# Alerts Resolved:\n\nLabels:\n - alertname = test\n - instance = vm1\n\nAnnotations:\n\nSource: \n\n\n\n"), Issuetype: &idNameValue{Name: "Incident"}, Labels: []string{"ALERT{6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b}", "alertmanager", "test"}, Project: &issueProject{Key: "OPS"}, @@ -722,8 +801,8 @@ func TestJiraNotify(t *testing.T) { { title: "error reopen transition not found", cfg: &config.JiraConfig{ - Summary: `{{ template "jira.default.summary" . }}`, - Description: `{{ template "jira.default.description" . }}`, + Summary: config.JiraFieldConfig{Template: `{{ template "jira.default.summary" . }}`}, + Description: config.JiraFieldConfig{Template: `{{ template "jira.default.description" . }}`}, IssueType: "Incident", Project: "OPS", Priority: `{{ template "jira.default.priority" . }}`, @@ -763,8 +842,8 @@ func TestJiraNotify(t *testing.T) { issue: issue{ Key: "", Fields: &issueFields{ - Summary: "[FIRING:1] test (vm1)", - Description: "\n\n# Alerts Firing:\n\nLabels:\n - alertname = test\n - instance = vm1\n\nAnnotations:\n\nSource: \n\n\n\n\n", + Summary: stringPtr("[FIRING:1] test (vm1)"), + Description: stringPtr("\n\n# Alerts Firing:\n\nLabels:\n - alertname = test\n - instance = vm1\n\nAnnotations:\n\nSource: \n\n\n\n\n"), Issuetype: &idNameValue{Name: "Incident"}, Labels: []string{"ALERT{6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b}", "alertmanager", "test"}, Project: &issueProject{Key: "OPS"}, @@ -861,10 +940,28 @@ func TestJiraNotify(t *testing.T) { t.Fatalf("unexpected method %s", r.Method) } + return + case "/issue/MONITORING-1": + body, err := io.ReadAll(r.Body) + if err != nil { + panic(err) + } + + var raw map[string]any + if err := json.Unmarshal(body, &raw); err != nil { + panic(err) + } + + if fields, ok := raw["fields"].(map[string]any); ok { + tc.customFieldAssetFn(t, fields) + } + + w.WriteHeader(http.StatusNoContent) return case "/issue/OPS-1": case "/issue/OPS-2": case "/issue/OPS-3": + case "/issue/OPS-4": fallthrough case "/issue": body, err := io.ReadAll(r.Body) diff --git a/notify/jira/types.go b/notify/jira/types.go index f636cca1f..ac933f81d 100644 --- a/notify/jira/types.go +++ b/notify/jira/types.go @@ -25,13 +25,13 @@ type issue struct { } type issueFields struct { - Description any `json:"description"` + Description *string `json:"description,omitempty"` Issuetype *idNameValue `json:"issuetype,omitempty"` Labels []string `json:"labels,omitempty"` Priority *idNameValue `json:"priority,omitempty"` Project *issueProject `json:"project,omitempty"` Resolution *idNameValue `json:"resolution,omitempty"` - Summary string `json:"summary"` + Summary *string `json:"summary,omitempty"` Status *issueStatus `json:"status,omitempty"` Fields map[string]any `json:"-"` @@ -69,11 +69,15 @@ type issueTransitions struct { // MarshalJSON merges the struct issueFields and issueFields.CustomField together. func (i issueFields) MarshalJSON() ([]byte, error) { - jsonFields := map[string]any{ - "description": i.Description, - "summary": i.Summary, + jsonFields := map[string]any{} + + if i.Summary != nil { + jsonFields["summary"] = *i.Summary } + if i.Description != nil { + jsonFields["description"] = *i.Description + } if i.Issuetype != nil { jsonFields["issuetype"] = i.Issuetype }