Skip to content

Commit

Permalink
v1
Browse files Browse the repository at this point in the history
Signed-off-by: mohamed <[email protected]>
  • Loading branch information
Mo-Fatah committed Nov 30, 2023
1 parent 85084d6 commit 4ed20a4
Show file tree
Hide file tree
Showing 7 changed files with 266 additions and 1 deletion.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2023 Armada
Copyright (c) 2023 Mohamed Abdelfatah

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
## CI Alerts

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:

- 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:

<img src="./example.png" width="400"/>

- If the job is triggered by a push to the `master` branch, the message will be sent to the channel with mentioning the channel.

## Usage Example
```yaml
name: Slack CI Alerts

on:
workflow_run:
workflows: [Test, Build, ...]
types: [completed]

jobs:
on-failure:
runs-on: ubuntu-latest
if: github.event.workflow_run.conclusion == 'failure'
steps:
- uses: actions/[email protected]
- name: "Send Notification"
uses: armadaproject/ci-alerts@v1
env:
webhook: ${{ secrets.SLACK_WEBHOOK }}
github_context: ${{ toJSON(github) }}
users_path: ${{github.workspace}}/.github/gh-to-slackid
```
### Environment Variables
- `webhook`: the slack webhook url. [see more](https://api.slack.com/messaging/webhooks).
- `github_context`: the github context. you can get it from `${{ toJSON(github) }}`.
- `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.

The format of the file should be as :
```
github-username:slack-id
```
11 changes: 11 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
name: ci-alerts
description: a workspace alerts for CI failures

runs:
using: 'composite'
steps:
- uses: actions/setup-go@v2
with:
go-version: '1.20'
- run: cd ${{ github.action_path }} && go mod tidy && go run main.go
shell: bash
Binary file added example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module github.com/armadaproject/ci-alerts

go 1.20

require github.com/antchfx/jsonquery v1.3.3

require (
github.com/antchfx/xpath v1.2.3 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
)
22 changes: 22 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
github.com/antchfx/jsonquery v1.3.3 h1:zjZpbnZhYng3uOAbIfdNq81A9mMEeuDJeYIpeKpZ4es=
github.com/antchfx/jsonquery v1.3.3/go.mod h1:1JG4DqRlRCHgVYDPY1ioYFAGSXGfWHzNgrbiGQHsWck=
github.com/antchfx/xpath v1.2.3 h1:CCZWOzv5bAqjVv0offZ2LVgVYFbeldKQVuLNbViZdes=
github.com/antchfx/xpath v1.2.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
181 changes: 181 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package main

import (
"bufio"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"

"github.com/antchfx/jsonquery"
)

func main() {
webhook := os.Getenv("webhook")
github_context := os.Getenv("github_context")
context, err := NewContext(webhook, github_context)
if err != nil {
log.Fatal(err)
}
message := buildMessage(context)
body := strings.NewReader(message)
_, err = http.Post(context.Webhook, "Content-type: application/json", body)
if err != nil {
log.Fatal(err)
}
}

type Context struct {
Webhook string
TriggeringEvent string
Branch string
Author string
Commit string
CommitUrl string
WorkflowName string
WorkflowUrl string
JobsUrl string
}

func NewContext(Webhook, github_context string) (*Context, error) {
jq, err := jsonquery.Parse(strings.NewReader(github_context))
if err != nil {
return nil, err
}

TriggeringEvent := jsonquery.FindOne(jq, "event/workflow_run/event").FirstChild.Data
Branch := jsonquery.FindOne(jq, "event/workflow_run/head_branch").FirstChild.Data
Author := jsonquery.FindOne(jq, "actor").FirstChild.Data
Commit := jsonquery.FindOne(jq, "sha").FirstChild.Data
repository := jsonquery.FindOne(jq, "repository").FirstChild.Data
CommitUrl := fmt.Sprintf("https://github.com/%s/commit/%s", repository, Commit)
WorkflowName := jsonquery.FindOne(jq, "event/workflow_run/name").FirstChild.Data
WorkflowUrl := jsonquery.FindOne(jq, "event/workflow_run/html_url").FirstChild.Data
JobsUrl := jsonquery.FindOne(jq, "event/workflow_run/jobs_url").FirstChild.Data

return &Context{
Webhook,
TriggeringEvent,
Branch,
Author,
Commit,
CommitUrl,
WorkflowName,
WorkflowUrl,
JobsUrl,
}, nil
}

func buildMessage(context *Context) string {
title := fmt.Sprintf("CI Failed For Branch: %s", context.Branch)
header := fmt.Sprintf(`{
"type" : "section",
"text" : {
"type": "mrkdwn",
"text": "*%s*\n %s"
}
},`, title, getMention(context))
section := buildSection(context)
message := fmt.Sprintf(`{"blocks" : [ %s ], "attachments":[{ "color": "#a60021", "blocks": [ %s ] }]}`, header, section)
return message
}

func buildSection(context *Context) string {
section := `{"type": "section", "fields":[`

failed_action := fmt.Sprintf(`
{
"type": "mrkdwn",
"text": "*Failed Action*\n%s"
},
`, context.WorkflowName)
section += failed_action

failed_job := fmt.Sprintf(`
{
"type": "mrkdwn",
"text": "*Failed Job*\n%s"
},
`, getFailedJob(context.JobsUrl))
section += failed_job

commit := fmt.Sprintf(`
{
"type": "mrkdwn",
"text": "*Commit*\n<%s|%s>"
},
`, context.CommitUrl, context.Commit[:6])
section += commit

action_url := fmt.Sprintf(`
{
"type": "mrkdwn",
"text": "*Workflow Url*\n%s"
}
`, context.WorkflowUrl)
section += action_url

section += `]}`
return section
}

func getMention(context *Context) string {
branch := strings.ToLower(context.Branch)
if branch == "main" || branch == "master" {
return "<!channel>"
} else if context.TriggeringEvent == "pull_request" || context.TriggeringEvent == "push" {
return getAuthorSlackID(context.Author)
}
return ""
}

func getAuthorSlackID(author string) string {
path := os.Getenv("users_path")
file, err := os.Open(path)
if err != nil {
return ""
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
lineArr := strings.Split(scanner.Text(), ":")
if len(lineArr) == 2 {
if lineArr[0] == author {
return fmt.Sprintf("<@%s>", lineArr[1])
}
}
}
os.Exit(0) // exit the program, to prevent any notification from unknown authors' PRs
return ""
}

func getFailedJob(url string) string {
res, err := http.Get(url)
if err != nil {
fmt.Println(err)
return ""
}
bodyBytes, err := io.ReadAll(res.Body)

if err != nil {
fmt.Println(err)
return ""
}

jq, err := jsonquery.Parse(strings.NewReader(string(bodyBytes)))
if err != nil {
fmt.Println(err)
return ""
}

jobs := jsonquery.FindOne(jq, "jobs").ChildNodes()
for _, job := range jobs {
status := job.SelectElement("conclusion").FirstChild.Data
if status == "failure" {
return job.SelectElement("name").FirstChild.Data
}
}
return ""
}

0 comments on commit 4ed20a4

Please sign in to comment.