diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4befed3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +.idea diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..99473c8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +FROM golang:1.17 + +WORKDIR /usr/src/notify-discord + +COPY go.mod go.sum ./ +RUN go mod download + +COPY config.go . +COPY description_part.go . +COPY main.go . +COPY validator.go . + +RUN mkdir bin + +RUN CGO_ENABLED=0 go build -a -o bin/notify-discord . + +FROM alpine:latest + +RUN apk update && apk add bash + +COPY --from=0 /usr/src/rancher-redeploy-workload/bin/notify-discord /usr/local/bin/notify-discord + +COPY docker-entrypoint.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + +CMD ["/usr/local/bin/docker-entrypoint.sh"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fe1968e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Gökhan Sarı + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..b89c43c --- /dev/null +++ b/action.yml @@ -0,0 +1,9 @@ +name: Notify Discord +description: An action for notifying Discord +author: Gokhan Sari +branding: + icon: send + color: blue +runs: + using: docker + image: docker://th0th/notify-discord:0.1 diff --git a/config.go b/config.go new file mode 100644 index 0000000..78221fb --- /dev/null +++ b/config.go @@ -0,0 +1,89 @@ +package main + +import ( + "errors" + "fmt" + "os" + "reflect" + "strings" +) + +type Config struct { + DiscordWebhookUrl string `env:"DISCORD_WEBHOOK_URL" validate:"required,url"` + GithubAction string `env:"GITHUB_ACTION" validate:"required"` + GithubJobStatus string `env:"GITHUB_JOB_STATUS" validate:"required"` + GithubRef string `env:"GITHUB_REF"` + GithubRepository string `env:"GITHUB_REPOSITORY" validate:"required"` + GithubRunId string `env:"GITHUB_RUN_ID"` + GithubServerUrl string `env:"GITHUB_SERVER_URL" validate:"required"` +} + +func NewConfig(v *Validator) (*Config, error) { + c := Config{} + + v, err := NewValidator() + if err != nil { + return nil, err + } + + t := reflect.TypeOf(c) + vp := reflect.ValueOf(&c) + + for i := 0; i < t.NumField(); i++ { + ti := t.Field(i) + vpi := vp.Elem().Field(i) + + envVarKey := ti.Tag.Get("env") + + if envVarKey != "" { + envVarValue := os.Getenv(envVarKey) + + vpi.SetString(envVarValue) + } + } + + err = v.Validate.Struct(c) + if err != nil { + output := "There are errors with some environment variables:\n" + + for fieldName, fieldMessage := range v.Map(err) { + structField, _ := t.FieldByName(fieldName) + + output = output + fmt.Sprintf("* %s: %s\n", structField.Tag.Get("env"), fieldMessage) + } + + return nil, errors.New(output) + } + + return &c, nil +} + +func (c *Config) GetRepositoryUrl() string { + return c.GithubServerUrl + "/" + c.GithubRepository +} + +func (c *Config) GetRefUrl() string { + if strings.HasPrefix(c.GithubRef, "refs/heads") { + s := strings.Split(c.GithubRef, "/") + branchName := s[len(s)-1] + + return c.GetRepositoryUrl() + "/tree/" + branchName + } + + if strings.HasPrefix(c.GithubRef, "refs/tags") { + s := strings.Split(c.GithubRef, "/") + tagName := s[len(s)-1] + + return c.GetRepositoryUrl() + "/releases/tags/" + tagName + } + + return "" +} + +func (c *Config) GetRunUrl() string { + if c.GithubRunId == "" { + return "" + } + + return c.GetRepositoryUrl() + "/runs/" + c.GithubRunId +} diff --git a/description_part.go b/description_part.go new file mode 100644 index 0000000..4f07961 --- /dev/null +++ b/description_part.go @@ -0,0 +1,26 @@ +package main + +import "fmt" + +type DescriptionPart struct { + Name string + Value string +} + +func (p DescriptionPart) ToString() string { + return fmt.Sprintf("**%s**: %s", p.Name, p.Value) +} + +func GetDescription(parts []DescriptionPart) string { + d := "" + + for i, part := range parts { + d += part.ToString() + + if i < len(parts) { + d += "\n\n" + } + } + + return d +} diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 0000000..59ee21a --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -eo pipefail + +exec /usr/local/bin/notify-discord diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..97a6ede --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module github.com/th0th/discord-notifier + +go 1.16 + +require ( + github.com/go-playground/locales v0.13.0 + github.com/go-playground/universal-translator v0.17.0 + github.com/go-playground/validator/v10 v10.7.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b9e924b --- /dev/null +++ b/go.sum @@ -0,0 +1,31 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.7.0 h1:gLi5ajTBBheLNt0ctewgq7eolXoDALQd5/y90Hh9ZgM= +github.com/go-playground/validator/v10 v10.7.0/go.mod h1:xm76BBt941f7yWdGnI2DVPFFg1UK3YY04qifoXU3lOk= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +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/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/main.go b/main.go new file mode 100644 index 0000000..c3dc96f --- /dev/null +++ b/main.go @@ -0,0 +1,72 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "net/http" +) + +func main() { + v, err := NewValidator() + if err != nil { + log.Panic(err) + } + + c, err := NewConfig(v) + if err != nil { + log.Panicln(err) + } + + var descriptionParts []DescriptionPart + + descriptionParts = append(descriptionParts, DescriptionPart{ + Name: "Repository", + Value: fmt.Sprintf("[%s](%s)", c.GithubRepository, c.GetRepositoryUrl()), + }) + + if c.GithubAction != "" { + descriptionParts = append(descriptionParts, DescriptionPart{ + Name: "Action", + Value: c.GithubAction, + }) + } + + embed := map[string]string{ + "description": GetDescription(descriptionParts), + } + + runUrl := c.GetRunUrl() + + if runUrl != "" { + embed["url"] = runUrl + } + + if c.GithubJobStatus == "success" { + embed["title"] = "Action is successful." + } else if c.GithubJobStatus == "failure" { + embed["title"] = "Action has failed." + } else if c.GithubJobStatus == "cancelled" { + embed["title"] = "Action is cancelled." + } + + payload := map[string]interface{}{ + "avatar_url": "https://user-images.githubusercontent.com/698079/126237897-88c5d9fb-a1d9-4421-955d-152c985726cf.png", + "embeds": []map[string]string{embed}, + } + + payloadByte, err := json.Marshal(payload) + + req, err := http.NewRequest(http.MethodPost, c.DiscordWebhookUrl, bytes.NewBuffer(payloadByte)) + if err != nil { + log.Panicln(err) + } + + req.Header.Set("content-type", "application/json") + + _, err = http.DefaultClient.Do(req) + if err != nil { + log.Panicln(err) + } +} diff --git a/validator.go b/validator.go new file mode 100644 index 0000000..a1dcf37 --- /dev/null +++ b/validator.go @@ -0,0 +1,94 @@ +package main + +import ( + "github.com/go-playground/locales/en" + "github.com/go-playground/universal-translator" + "github.com/go-playground/validator/v10" + enTranslations "github.com/go-playground/validator/v10/translations/en" + "log" +) + +type Validator struct { + Translator ut.Translator + Validate *validator.Validate +} + +func NewValidator() (*Validator, error) { + validate := validator.New() + + enLocale := en.New() + u := ut.New(enLocale, enLocale) + + trans, _ := u.GetTranslator("en") + + err := enTranslations.RegisterDefaultTranslations(validate, trans) + if err != nil { + return nil, err + } + + v := &Validator{ + Translator: trans, + Validate: validate, + } + + err = v.RegisterTranslations() + if err != nil { + return nil, err + } + + return v, nil +} + +func (v *Validator) Map(err error) map[string]interface{} { + m := map[string]interface{}{} + + validationErrors := err.(validator.ValidationErrors) + + for _, e := range validationErrors { + m[e.Field()] = e.Translate(v.Translator) + } + + return m +} + +func (v *Validator) RegisterTranslations() error { + // required + err := v.Validate.RegisterTranslation( + "required", + v.Translator, + func(u ut.Translator) error { + return u.Add("required", "This field is required.", true) + }, + func(u ut.Translator, fe validator.FieldError) string { + t, err := u.T("required", fe.Field()) + if err != nil { + log.Panic(err) + } + + return t + }) + if err != nil { + return err + } + + // url + err = v.Validate.RegisterTranslation( + "url", + v.Translator, + func(u ut.Translator) error { + return u.Add("url", "This field should be a valid URL.", true) + }, + func(u ut.Translator, fe validator.FieldError) string { + t, err := u.T("url", fe.Field()) + if err != nil { + log.Panic(err) + } + + return t + }) + if err != nil { + return err + } + + return nil +}