Skip to content

Commit 14e532a

Browse files
feat(jira): make Jira fields configurable to control which ones are updated
Signed-off-by: Holger Waschke <[email protected]>
1 parent fd259d3 commit 14e532a

File tree

5 files changed

+231
-71
lines changed

5 files changed

+231
-71
lines changed

config/notifiers.go

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -206,10 +206,14 @@ var (
206206
NotifierConfig: NotifierConfig{
207207
VSendResolved: true,
208208
},
209-
APIType: "auto",
210-
Summary: `{{ template "jira.default.summary" . }}`,
211-
Description: `{{ template "jira.default.description" . }}`,
212-
Priority: `{{ template "jira.default.priority" . }}`,
209+
APIType: "auto",
210+
Summary: JiraFieldConfig{
211+
Template: `{{ template "jira.default.summary" . }}`,
212+
},
213+
Description: JiraFieldConfig{
214+
Template: `{{ template "jira.default.description" . }}`,
215+
},
216+
Priority: `{{ template "jira.default.priority" . }}`,
213217
}
214218

215219
DefaultMattermostConfig = MattermostConfig{
@@ -969,19 +973,26 @@ func (c *MSTeamsV2Config) UnmarshalYAML(unmarshal func(any) error) error {
969973
return nil
970974
}
971975

976+
type JiraFieldConfig struct {
977+
// Template is the template string used to render the field.
978+
Template string `yaml:"template,omitempty" json:"template,omitempty"`
979+
// DisableUpdate indicates whether this field should be omitted when updating an existing issue.
980+
DisableUpdate bool `yaml:"disable_update,omitempty" json:"disable_update,omitempty"`
981+
}
982+
972983
type JiraConfig struct {
973984
NotifierConfig `yaml:",inline" json:",inline"`
974985
HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"`
975986

976987
APIURL *URL `yaml:"api_url,omitempty" json:"api_url,omitempty"`
977988
APIType string `yaml:"api_type,omitempty" json:"api_type,omitempty"`
978989

979-
Project string `yaml:"project,omitempty" json:"project,omitempty"`
980-
Summary string `yaml:"summary,omitempty" json:"summary,omitempty"`
981-
Description string `yaml:"description,omitempty" json:"description,omitempty"`
982-
Labels []string `yaml:"labels,omitempty" json:"labels,omitempty"`
983-
Priority string `yaml:"priority,omitempty" json:"priority,omitempty"`
984-
IssueType string `yaml:"issue_type,omitempty" json:"issue_type,omitempty"`
990+
Project string `yaml:"project,omitempty" json:"project,omitempty"`
991+
Summary JiraFieldConfig `yaml:"summary,omitempty" json:"summary,omitempty"`
992+
Description JiraFieldConfig `yaml:"description,omitempty" json:"description,omitempty"`
993+
Labels []string `yaml:"labels,omitempty" json:"labels,omitempty"`
994+
Priority string `yaml:"priority,omitempty" json:"priority,omitempty"`
995+
IssueType string `yaml:"issue_type,omitempty" json:"issue_type,omitempty"`
985996

986997
ReopenTransition string `yaml:"reopen_transition,omitempty" json:"reopen_transition,omitempty"`
987998
ResolveTransition string `yaml:"resolve_transition,omitempty" json:"resolve_transition,omitempty"`
@@ -991,6 +1002,21 @@ type JiraConfig struct {
9911002
Fields map[string]any `yaml:"fields,omitempty" json:"custom_fields,omitempty"`
9921003
}
9931004

1005+
// Supports both the legacy string and the new object form.
1006+
func (f *JiraFieldConfig) UnmarshalYAML(unmarshal func(any) error) error {
1007+
// Try simple string first (backward compatibility).
1008+
var s string
1009+
if err := unmarshal(&s); err == nil {
1010+
f.Template = s
1011+
// DisableUpdate stays false by default.
1012+
return nil
1013+
}
1014+
1015+
// Fallback to full object form.
1016+
type plain JiraFieldConfig
1017+
return unmarshal((*plain)(f))
1018+
}
1019+
9941020
func (c *JiraConfig) UnmarshalYAML(unmarshal func(any) error) error {
9951021
*c = DefaultJiraConfig
9961022
type plain JiraConfig

docs/configuration.md

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1159,11 +1159,23 @@ The default `jira.default.description` template only works with V2.
11591159
# The project key where issues are created.
11601160
project: <string>
11611161
1162-
# Issue summary template.
1163-
[ summary: <tmpl_string> | default = '{{ template "jira.default.summary" . }}' ]
1164-
1165-
# Issue description template.
1166-
[ description: <tmpl_string> | default = '{{ template "jira.default.description" . }}' ]
1162+
# Issue summary configuration.
1163+
[ summary:
1164+
# Template for the issue summary.
1165+
[ template: <tmpl_string> | default = '{{ template "jira.default.summary" . }}' ]
1166+
1167+
# If true, the summary will not be updated when updating an existing issue.
1168+
[ disable_update: <boolean> | default = false ]
1169+
]
1170+
1171+
# Issue description configuration.
1172+
[ description:
1173+
# Template for the issue description.
1174+
[ template: <tmpl_string> | default = '{{ template "jira.default.description" . }}' ]
1175+
1176+
# If true, the description will not be updated when updating an existing issue.
1177+
[ disable_update: <boolean> | default = false ]
1178+
]
11671179
11681180
# Labels to be added to the issue.
11691181
labels:

notify/jira/jira.go

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -104,14 +104,35 @@ func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error)
104104
path = "issue/" + existingIssue.Key
105105
method = http.MethodPut
106106

107-
logger.Debug("updating existing issue", "issue_key", existingIssue.Key)
107+
msg := "updating existing issue"
108+
var disabled []string
109+
if n.conf.Summary.DisableUpdate {
110+
disabled = append(disabled, "summary")
111+
}
112+
if n.conf.Description.DisableUpdate {
113+
disabled = append(disabled, "description")
114+
}
115+
if len(disabled) > 0 {
116+
msg += " without " + strings.Join(disabled, " and ")
117+
}
118+
119+
logger.Debug(msg, "issue_key", existingIssue.Key)
108120
}
109121

110122
requestBody, err := n.prepareIssueRequestBody(ctx, logger, key.Hash(), tmplTextFunc)
111123
if err != nil {
112124
return false, err
113125
}
114126

127+
if method == http.MethodPut && requestBody.Fields != nil {
128+
if n.conf.Description.DisableUpdate {
129+
requestBody.Fields.Description = nil
130+
}
131+
if n.conf.Summary.DisableUpdate {
132+
requestBody.Fields.Summary = nil
133+
}
134+
}
135+
115136
_, shouldRetry, err = n.doAPIRequest(ctx, method, path, requestBody)
116137
if err != nil {
117138
return shouldRetry, fmt.Errorf("failed to %s request to %q: %w", method, path, err)
@@ -121,10 +142,11 @@ func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error)
121142
}
122143

123144
func (n *Notifier) prepareIssueRequestBody(_ context.Context, logger *slog.Logger, groupID string, tmplTextFunc template.TemplateFunc) (issue, error) {
124-
summary, err := tmplTextFunc(n.conf.Summary)
145+
summary, err := tmplTextFunc(n.conf.Summary.Template)
125146
if err != nil {
126147
return issue{}, fmt.Errorf("summary template: %w", err)
127148
}
149+
128150
project, err := tmplTextFunc(n.conf.Project)
129151
if err != nil {
130152
return issue{}, fmt.Errorf("project template: %w", err)
@@ -156,12 +178,12 @@ func (n *Notifier) prepareIssueRequestBody(_ context.Context, logger *slog.Logge
156178
requestBody := issue{Fields: &issueFields{
157179
Project: &issueProject{Key: project},
158180
Issuetype: &idNameValue{Name: issueType},
159-
Summary: summary,
181+
Summary: &summary,
160182
Labels: make([]string, 0, len(n.conf.Labels)+1),
161183
Fields: fieldsWithStringKeys,
162184
}}
163185

164-
issueDescriptionString, err := tmplTextFunc(n.conf.Description)
186+
issueDescriptionString, err := tmplTextFunc(n.conf.Description.Template)
165187
if err != nil {
166188
return issue{}, fmt.Errorf("description template: %w", err)
167189
}
@@ -171,14 +193,13 @@ func (n *Notifier) prepareIssueRequestBody(_ context.Context, logger *slog.Logge
171193
logger.Warn("Truncated description", "max_runes", maxDescriptionLenRunes)
172194
}
173195

174-
requestBody.Fields.Description = issueDescriptionString
175-
if strings.HasSuffix(n.conf.APIURL.Path, "/3") {
176-
var issueDescription any
177-
if err := json.Unmarshal([]byte(issueDescriptionString), &issueDescription); err != nil {
178-
return issue{}, fmt.Errorf("description unmarshaling: %w", err)
196+
descriptionCopy := issueDescriptionString
197+
if isAPIv3Path(n.conf.APIURL.Path) {
198+
if !json.Valid([]byte(descriptionCopy)) {
199+
return issue{}, fmt.Errorf("description template: invalid JSON for API v3")
179200
}
180-
requestBody.Fields.Description = issueDescription
181201
}
202+
requestBody.Fields.Description = &descriptionCopy
182203

183204
for i, label := range n.conf.Labels {
184205
label, err = tmplTextFunc(label)
@@ -395,3 +416,7 @@ func (n *Notifier) doAPIRequestFullPath(ctx context.Context, method, path string
395416

396417
return responseBody, false, nil
397418
}
419+
420+
func isAPIv3Path(path string) bool {
421+
return strings.HasSuffix(strings.TrimRight(path, "/"), "/3")
422+
}

0 commit comments

Comments
 (0)