Skip to content

Commit 4ed20a4

Browse files
committed
v1
Signed-off-by: mohamed <[email protected]>
1 parent 85084d6 commit 4ed20a4

File tree

7 files changed

+266
-1
lines changed

7 files changed

+266
-1
lines changed

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2023 Armada
3+
Copyright (c) 2023 Mohamed Abdelfatah
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
## CI Alerts
2+
3+
a simple action to customize Slack alerts for CI failures using Slack [Webhooks](https://api.slack.com/messaging/webhooks). Simply, the action will send a message to a slack channel when a CI job fails in two cases:
4+
5+
- If the job is triggered by a pull_request AND the author of the PR is in the `users_path` file, the author will be mentioned in the message. the message will be as the following:
6+
7+
<img src="./example.png" width="400"/>
8+
9+
- If the job is triggered by a push to the `master` branch, the message will be sent to the channel with mentioning the channel.
10+
11+
## Usage Example
12+
```yaml
13+
name: Slack CI Alerts
14+
15+
on:
16+
workflow_run:
17+
workflows: [Test, Build, ...]
18+
types: [completed]
19+
20+
jobs:
21+
on-failure:
22+
runs-on: ubuntu-latest
23+
if: github.event.workflow_run.conclusion == 'failure'
24+
steps:
25+
- uses: actions/[email protected]
26+
- name: "Send Notification"
27+
uses: armadaproject/ci-alerts@v1
28+
env:
29+
webhook: ${{ secrets.SLACK_WEBHOOK }}
30+
github_context: ${{ toJSON(github) }}
31+
users_path: ${{github.workspace}}/.github/gh-to-slackid
32+
```
33+
### Environment Variables
34+
- `webhook`: the slack webhook url. [see more](https://api.slack.com/messaging/webhooks).
35+
- `github_context`: the github context. you can get it from `${{ toJSON(github) }}`.
36+
- `users_path`: the path to the file that contains the mapping from a github username to a slack [client id](https://api.slack.com/authentication/best-practices#client-id) so the author can be mentioned in the message. If the author doesn't exist in the file, no message will be sent to the slack channel.
37+
38+
The format of the file should be as :
39+
```
40+
github-username:slack-id
41+
```

action.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
name: ci-alerts
2+
description: a workspace alerts for CI failures
3+
4+
runs:
5+
using: 'composite'
6+
steps:
7+
- uses: actions/setup-go@v2
8+
with:
9+
go-version: '1.20'
10+
- run: cd ${{ github.action_path }} && go mod tidy && go run main.go
11+
shell: bash

example.png

77.5 KB
Loading

go.mod

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module github.com/armadaproject/ci-alerts
2+
3+
go 1.20
4+
5+
require github.com/antchfx/jsonquery v1.3.3
6+
7+
require (
8+
github.com/antchfx/xpath v1.2.3 // indirect
9+
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
10+
)

go.sum

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
github.com/antchfx/jsonquery v1.3.3 h1:zjZpbnZhYng3uOAbIfdNq81A9mMEeuDJeYIpeKpZ4es=
2+
github.com/antchfx/jsonquery v1.3.3/go.mod h1:1JG4DqRlRCHgVYDPY1ioYFAGSXGfWHzNgrbiGQHsWck=
3+
github.com/antchfx/xpath v1.2.3 h1:CCZWOzv5bAqjVv0offZ2LVgVYFbeldKQVuLNbViZdes=
4+
github.com/antchfx/xpath v1.2.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
5+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
7+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
8+
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
9+
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
10+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
11+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
12+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
13+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
14+
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
15+
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
16+
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
17+
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
18+
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
19+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
20+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
21+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
22+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

main.go

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"fmt"
6+
"io"
7+
"log"
8+
"net/http"
9+
"os"
10+
"strings"
11+
12+
"github.com/antchfx/jsonquery"
13+
)
14+
15+
func main() {
16+
webhook := os.Getenv("webhook")
17+
github_context := os.Getenv("github_context")
18+
context, err := NewContext(webhook, github_context)
19+
if err != nil {
20+
log.Fatal(err)
21+
}
22+
message := buildMessage(context)
23+
body := strings.NewReader(message)
24+
_, err = http.Post(context.Webhook, "Content-type: application/json", body)
25+
if err != nil {
26+
log.Fatal(err)
27+
}
28+
}
29+
30+
type Context struct {
31+
Webhook string
32+
TriggeringEvent string
33+
Branch string
34+
Author string
35+
Commit string
36+
CommitUrl string
37+
WorkflowName string
38+
WorkflowUrl string
39+
JobsUrl string
40+
}
41+
42+
func NewContext(Webhook, github_context string) (*Context, error) {
43+
jq, err := jsonquery.Parse(strings.NewReader(github_context))
44+
if err != nil {
45+
return nil, err
46+
}
47+
48+
TriggeringEvent := jsonquery.FindOne(jq, "event/workflow_run/event").FirstChild.Data
49+
Branch := jsonquery.FindOne(jq, "event/workflow_run/head_branch").FirstChild.Data
50+
Author := jsonquery.FindOne(jq, "actor").FirstChild.Data
51+
Commit := jsonquery.FindOne(jq, "sha").FirstChild.Data
52+
repository := jsonquery.FindOne(jq, "repository").FirstChild.Data
53+
CommitUrl := fmt.Sprintf("https://github.com/%s/commit/%s", repository, Commit)
54+
WorkflowName := jsonquery.FindOne(jq, "event/workflow_run/name").FirstChild.Data
55+
WorkflowUrl := jsonquery.FindOne(jq, "event/workflow_run/html_url").FirstChild.Data
56+
JobsUrl := jsonquery.FindOne(jq, "event/workflow_run/jobs_url").FirstChild.Data
57+
58+
return &Context{
59+
Webhook,
60+
TriggeringEvent,
61+
Branch,
62+
Author,
63+
Commit,
64+
CommitUrl,
65+
WorkflowName,
66+
WorkflowUrl,
67+
JobsUrl,
68+
}, nil
69+
}
70+
71+
func buildMessage(context *Context) string {
72+
title := fmt.Sprintf("CI Failed For Branch: %s", context.Branch)
73+
header := fmt.Sprintf(`{
74+
"type" : "section",
75+
"text" : {
76+
"type": "mrkdwn",
77+
"text": "*%s*\n %s"
78+
}
79+
},`, title, getMention(context))
80+
section := buildSection(context)
81+
message := fmt.Sprintf(`{"blocks" : [ %s ], "attachments":[{ "color": "#a60021", "blocks": [ %s ] }]}`, header, section)
82+
return message
83+
}
84+
85+
func buildSection(context *Context) string {
86+
section := `{"type": "section", "fields":[`
87+
88+
failed_action := fmt.Sprintf(`
89+
{
90+
"type": "mrkdwn",
91+
"text": "*Failed Action*\n%s"
92+
},
93+
`, context.WorkflowName)
94+
section += failed_action
95+
96+
failed_job := fmt.Sprintf(`
97+
{
98+
"type": "mrkdwn",
99+
"text": "*Failed Job*\n%s"
100+
},
101+
`, getFailedJob(context.JobsUrl))
102+
section += failed_job
103+
104+
commit := fmt.Sprintf(`
105+
{
106+
"type": "mrkdwn",
107+
"text": "*Commit*\n<%s|%s>"
108+
},
109+
`, context.CommitUrl, context.Commit[:6])
110+
section += commit
111+
112+
action_url := fmt.Sprintf(`
113+
{
114+
"type": "mrkdwn",
115+
"text": "*Workflow Url*\n%s"
116+
}
117+
`, context.WorkflowUrl)
118+
section += action_url
119+
120+
section += `]}`
121+
return section
122+
}
123+
124+
func getMention(context *Context) string {
125+
branch := strings.ToLower(context.Branch)
126+
if branch == "main" || branch == "master" {
127+
return "<!channel>"
128+
} else if context.TriggeringEvent == "pull_request" || context.TriggeringEvent == "push" {
129+
return getAuthorSlackID(context.Author)
130+
}
131+
return ""
132+
}
133+
134+
func getAuthorSlackID(author string) string {
135+
path := os.Getenv("users_path")
136+
file, err := os.Open(path)
137+
if err != nil {
138+
return ""
139+
}
140+
defer file.Close()
141+
scanner := bufio.NewScanner(file)
142+
for scanner.Scan() {
143+
lineArr := strings.Split(scanner.Text(), ":")
144+
if len(lineArr) == 2 {
145+
if lineArr[0] == author {
146+
return fmt.Sprintf("<@%s>", lineArr[1])
147+
}
148+
}
149+
}
150+
os.Exit(0) // exit the program, to prevent any notification from unknown authors' PRs
151+
return ""
152+
}
153+
154+
func getFailedJob(url string) string {
155+
res, err := http.Get(url)
156+
if err != nil {
157+
fmt.Println(err)
158+
return ""
159+
}
160+
bodyBytes, err := io.ReadAll(res.Body)
161+
162+
if err != nil {
163+
fmt.Println(err)
164+
return ""
165+
}
166+
167+
jq, err := jsonquery.Parse(strings.NewReader(string(bodyBytes)))
168+
if err != nil {
169+
fmt.Println(err)
170+
return ""
171+
}
172+
173+
jobs := jsonquery.FindOne(jq, "jobs").ChildNodes()
174+
for _, job := range jobs {
175+
status := job.SelectElement("conclusion").FirstChild.Data
176+
if status == "failure" {
177+
return job.SelectElement("name").FirstChild.Data
178+
}
179+
}
180+
return ""
181+
}

0 commit comments

Comments
 (0)