Skip to content

Commit c48316a

Browse files
committed
Add support for skipping auto-close of issues based on annotation
1 parent 1199ccd commit c48316a

File tree

6 files changed

+141
-3
lines changed

6 files changed

+141
-3
lines changed

README.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ OPTIONS:
8585
--github-app-installation-id value GitHub App installation ID (default: 0) [$ATG_GITHUB_APP_INSTALLATION_ID]
8686
--github-app-private-key value GitHub App private key (command line argument is not recommended) [$ATG_GITHUB_APP_PRIVATE_KEY]
8787
--github-token value GitHub API token (command line argument is not recommended) [$ATG_GITHUB_TOKEN]
88-
--auto-close-resolved-issues Should issues be automatically closed when resolved (default: true) [$ATG_AUTO_CLOSE_RESOLVED_ISSUES]
88+
--auto-close-resolved-issues Should issues be automatically closed when resolved. If alerts have 'atg-skip-auto-close=true' annotation, issues will not be auto-closed. (default: true) [$ATG_AUTO_CLOSE_RESOLVED_ISSUES]
8989
--reopen-window value Alerts will create a new issue instead of reopening closed issues if the specified duration has passed [$ATG_REOPEN_WINDOW]
9090
--help, -h show help
9191
```
@@ -106,6 +106,21 @@ Issue title and body are rendered from [Go template](https://golang.org/pkg/text
106106
- `json`: Marshal an object to JSON string
107107
- `timeNow`: Get current time
108108
109+
### Automatically close issues when alerts are resolved
110+
111+
You can use the `--auto-close-resolved-issues` flag to automatically close issues when alerts are resolved.
112+
113+
If you want to skip auto-close for some alerts, add the `atg-skip-auto-close=true` annotation to them.
114+
115+
```yaml
116+
- alert: HighRequestLatency
117+
expr: job:request_latency_seconds:mean5m{job="myjob"} > 0.5
118+
labels:
119+
severity: critical
120+
annotations:
121+
atg-skip-auto-close: "true"
122+
```
123+
109124
## Customize organization and repository
110125
111126
The organization/repository where issues are raised can be customized per-alert by specifying the `atg_owner` label for the organization and/or the `atg_repo` label for the repository on the alert.

pkg/cli/cli.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ func App() *cli.App {
181181
Name: flagAutoCloseResolvedIssues,
182182
Required: false,
183183
Value: true,
184-
Usage: "Should issues be automatically closed when resolved",
184+
Usage: "Should issues be automatically closed when resolved. If alerts have 'atg-skip-auto-close=true' annotation, issues will not be auto-closed.",
185185
EnvVars: []string{"ATG_AUTO_CLOSE_RESOLVED_ISSUES"},
186186
},
187187
&noDefaultDurationFlag{

pkg/cli/templates/body.tmpl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,9 @@ Previous Issue: {{ $previousIssue.HTMLURL }}
5555
{{end -}}
5656
</table>
5757

58+
{{- if $payload.HasSkipAutoCloseAnnotation }}
59+
60+
*This issue will not be auto-closed because the alerts have `atg-skip-auto-close=true` annotation.*
61+
{{- end }}
62+
5863
<!-- alert data: {{json $payload}} -->

pkg/notifier/github.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ func (n *GitHubNotifier) Notify(ctx context.Context, payload *types.WebhookPaylo
220220
}
221221

222222
currentState := issue.GetState()
223-
canUpdateState := desiredState != "closed" || n.AutoCloseResolvedIssues
223+
canUpdateState := desiredState == "open" || n.shouldAutoCloseIssue(payload)
224224

225225
if desiredState != currentState && canUpdateState {
226226
req = &github.IssueRequest{
@@ -300,6 +300,14 @@ func (n *GitHubNotifier) getAlertID(payload *types.WebhookPayload) (string, erro
300300
return fmt.Sprintf("%x", sha256.Sum256([]byte(id))), nil
301301
}
302302

303+
func (n *GitHubNotifier) shouldAutoCloseIssue(payload *types.WebhookPayload) bool {
304+
if !n.AutoCloseResolvedIssues {
305+
return false
306+
}
307+
308+
return !payload.HasSkipAutoCloseAnnotation()
309+
}
310+
303311
func checkSearchResponse(response *github.Response) error {
304312
if response.StatusCode < 200 || 300 <= response.StatusCode {
305313
return fmt.Errorf("issue search returned %d", response.StatusCode)

pkg/types/payload.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ type AlertStatus string
1010
const (
1111
AlertStatusResolved AlertStatus = "resolved"
1212
AlertStatusFiring AlertStatus = "firing"
13+
14+
skipAutoCloseAnnotationKey = "atg-skip-auto-close"
15+
skipAutoCloseAnnotationValue = "true"
1316
)
1417

1518
type WebhookPayload struct {
@@ -77,3 +80,18 @@ func (p *WebhookPayload) AnnotationKeysExceptCommon() []string {
7780

7881
return keys
7982
}
83+
84+
func (p *WebhookPayload) HasSkipAutoCloseAnnotation() bool {
85+
for _, alert := range p.Alerts {
86+
if alert.Annotations == nil {
87+
continue
88+
}
89+
90+
val, ok := alert.Annotations[skipAutoCloseAnnotationKey]
91+
if ok && val == skipAutoCloseAnnotationValue {
92+
return true
93+
}
94+
}
95+
96+
return false
97+
}

pkg/types/payload_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package types
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestWebhookPayloadHasSkipAutoCloseAnnotation(t *testing.T) {
10+
tests := []struct {
11+
name string
12+
payload *WebhookPayload
13+
expected bool
14+
}{
15+
{
16+
name: "no annotations",
17+
payload: &WebhookPayload{
18+
Alerts: []WebhookAlert{{
19+
Labels: map[string]string{
20+
"job": "example",
21+
},
22+
}},
23+
},
24+
expected: false,
25+
},
26+
{
27+
name: "has the annotation",
28+
payload: &WebhookPayload{
29+
Alerts: []WebhookAlert{{
30+
Annotations: map[string]string{
31+
"atg-skip-auto-close": "true",
32+
},
33+
}},
34+
},
35+
expected: true,
36+
},
37+
{
38+
name: "don't has the annotation",
39+
payload: &WebhookPayload{
40+
Alerts: []WebhookAlert{{
41+
Annotations: map[string]string{
42+
"description": "example",
43+
},
44+
}},
45+
},
46+
expected: false,
47+
},
48+
{
49+
name: "no alerts has the annotation",
50+
payload: &WebhookPayload{
51+
Alerts: []WebhookAlert{
52+
{
53+
Labels: map[string]string{
54+
"job": "example",
55+
},
56+
},
57+
{
58+
Labels: map[string]string{
59+
"job": "example",
60+
},
61+
},
62+
},
63+
},
64+
expected: false,
65+
},
66+
{
67+
name: "some alerts have the annotation",
68+
payload: &WebhookPayload{
69+
Alerts: []WebhookAlert{
70+
{
71+
Annotations: map[string]string{
72+
"atg-skip-auto-close": "true",
73+
},
74+
},
75+
{
76+
Labels: map[string]string{
77+
"job": "example",
78+
},
79+
},
80+
},
81+
},
82+
expected: true,
83+
},
84+
}
85+
86+
for _, tt := range tests {
87+
t.Run(tt.name, func(t *testing.T) {
88+
actual := tt.payload.HasSkipAutoCloseAnnotation()
89+
assert.Equal(t, tt.expected, actual)
90+
})
91+
}
92+
}

0 commit comments

Comments
 (0)