From 989331579694a557f159bcaaa7b7a71a90f6a530 Mon Sep 17 00:00:00 2001 From: "Stanislav N. aka pztrn" Date: Wed, 16 Oct 2019 20:19:23 +0500 Subject: [PATCH] Initial commit. Should work, but no promises (yet). --- README.md | 3 + env/env.go | 143 ++++++++++++++++++++++++++++++++++++++++ go.mod | 10 +++ go.sum | 38 +++++++++++ main.go | 24 +++++++ message/payload.go | 43 ++++++++++++ message/prepare.go | 8 +++ message/process.go | 55 ++++++++++++++++ message/send_message.go | 28 ++++++++ message/template.go | 79 ++++++++++++++++++++++ 10 files changed, 431 insertions(+) create mode 100644 README.md create mode 100644 env/env.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 message/payload.go create mode 100644 message/prepare.go create mode 100644 message/process.go create mode 100644 message/send_message.go create mode 100644 message/template.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..4306d32 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Discordrone + +This is Drone CI plugin for pushing messages about CI/CD things into Discord channels using webhooks. diff --git a/env/env.go b/env/env.go new file mode 100644 index 0000000..237c5ed --- /dev/null +++ b/env/env.go @@ -0,0 +1,143 @@ +package env + +import ( + // other + "github.com/vrischmann/envconfig" + + "fmt" +) + +var Data *environmentData + +type environmentData struct { + CI bool `envconfig:"optional"` + IsDrone bool `envconfig:"DRONE,optional"` + Debug bool `envconfig:"optional"` + + Drone struct { + Branch string `envconfig:"optional"` + Build struct { + Action string `envconfig:"optional"` + Created int `envconfig:"optional"` + Event string `envconfig:"optional"` + Finished int `envconfig:"optional"` + Link string `envconfig:"optional"` + Number int `envconfig:"optional"` + Parent string `envconfig:"optional"` + Started int `envconfig:"optional"` + Status string `envconfig:"optional"` + } + CommitHash string `envconfig:"DRONE_COMMIT,optional"` + Commit struct { + After string `envconfig:"optional"` + AuthorName string `envconfig:"DRONE_COMMIT_AUTHOR,optional"` + Author struct { + Avatar string `envconfig:"optional"` + Email string `envconfig:"optional"` + Name string `envconfig:"optional"` + } + Before string `envconfig:"optional"` + Branch string `envconfig:"optional"` + Link string `envconfig:"optional"` + Message string `envconfig:"optional"` + Ref string `envconfig:"optional"` + Sha string `envconfig:"optional"` + } + Deploy struct { + To string `envconfig:"optional"` + } + Failed struct { + Stages string `envconfig:"optional"` + Steps string `envconfig:"optional"` + } + Git struct { + HTTP struct { + URL string `envconfig:"optional"` + } + SSH struct { + URL string `envconfig:"optional"` + } + } + Pull struct { + Request string `envconfig:"optional"` + } + Remote struct { + URL string `envconfig:"optional"` + } + RepoName string `envconfig:"DRONE_REPO_NAME,optional"` + Repo struct { + Branch string `envconfig:"optional"` + Link string `envconfig:"optional"` + Name string `envconfig:"optional"` + Namespace string `envconfig:"optional"` + Owner string `envconfig:"optional"` + Private bool `envconfig:"optional"` + SCM string `envconfig:"optional"` + Visibility string `envconfig:"optional"` + } + SemverData string `envconfig:"DRONE_SEMVER,optional"` + Semver struct { + Build string `envconfig:"optional"` + Error string `envconfig:"optional"` + Major int `envconfig:"optional"` + Minor int `envconfig:"optional"` + Patch int `envconfig:"optional"` + Prerelease string `envconfig:"optional"` + Short string `envconfig:"optional"` + } + Source struct { + Branch string `envconfig:"optional"` + } + Stage struct { + Arch string `envconfig:"optional"` + Depends struct { + On string `envconfig:"optional"` + } + Finished int `envconfig:"optional"` + Kind string `envconfig:"optional"` + Machine string `envconfig:"optional"` + Name string `envconfig:"optional"` + Number int `envconfig:"optional"` + OS string `envconfig:"optional"` + Started int `envconfig:"optional"` + Status string `envconfig:"optional"` + Type string `envconfig:"optional"` + Variant string `envconfig:"optional"` + } + Step struct { + Name string `envconfig:"optional"` + Number int `envconfig:"optional"` + } + System struct { + Host string `envconfig:"optional"` + Hostname string `envconfig:"optional"` + Proto string `envconfig:"optional"` + Version string `envconfig:"optional"` + } + Tag string `envconfig:"optional"` + Target struct { + Branch string `envconfig:"optional"` + } + } + Plugin struct { + Avatar struct { + URL string `envconfig:"optional"` + } + Color string `envconfig:"optional"` + Message string `envconfig:"optional"` + Webhook struct { + ID string `envconfig:"optional"` + Token string `envconfig:"optional"` + } + TTS bool `envconfig:"optional"` + Username string `envconfig:"optional"` + } +} + +func ParseEnv() { + Data = &environmentData{} + err := envconfig.Init(Data) + if err != nil { + fmt.Println(err) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3c46dc4 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module go.dev.pztrn.name/discordrone + +go 1.13 + +require ( + github.com/drone/drone-template-lib v1.0.0 + github.com/stretchr/testify v1.4.0 // indirect + github.com/vrischmann/envconfig v1.2.0 + gopkg.in/yaml.v2 v2.2.4 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2b5ea83 --- /dev/null +++ b/go.sum @@ -0,0 +1,38 @@ +github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg= +github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc= +github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/sprig v2.18.0+incompatible h1:QoGhlbC6pter1jxKnjMFxT8EqsLuDE6FEcNbWEpw+lI= +github.com/Masterminds/sprig v2.18.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= +github.com/aymerick/raymond v2.0.2+incompatible h1:VEp3GpgdAnv9B2GFyTvqgcKvY+mfKMjPOA3SbKLtnU0= +github.com/aymerick/raymond v2.0.2+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= +github.com/bouk/monkey v1.0.0 h1:k6z8fLlPhETfn5l9rlWVE7Q6B23DoaqosTdArvNQRdc= +github.com/bouk/monkey v1.0.0/go.mod h1:PG/63f4XEUlVyW1ttIeOJmJhhe1+t9EC/je3eTjvFhE= +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/drone/drone-template-lib v1.0.0 h1:PNBBfUhifRnrPCoWBlTitk3jipXdv8u8WLbIf7h7j00= +github.com/drone/drone-template-lib v1.0.0/go.mod h1:Hqy1tgqPH5mtbFOZmow19l4jOkZvp+WZ00cB4W3MJhg= +github.com/google/uuid v1.1.0 h1:Jf4mxPC/ziBnoPIdpQdPJ9OeiomAUHLvxmPRSPH9m4s= +github.com/google/uuid v1.1.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/huandu/xstrings v1.2.0 h1:yPeWdRnmynF7p+lLYz0H2tthW9lqhMJrQV/U7yy4wX0= +github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= +github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI= +github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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= +github.com/tkuchiki/faketime v0.0.0-20170607100027-a4500a4f4643 h1:ii/sHfgFMByozryLeiDmn1ClZ/Pena4NgpJ4P7UuX9o= +github.com/tkuchiki/faketime v0.0.0-20170607100027-a4500a4f4643/go.mod h1:RXY/TXAwGGL36IKDjrHFMcjpUrEiyWSEtLhFPw3UWF0= +github.com/vrischmann/envconfig v1.2.0 h1:5/u4fI34/g3m0SdTQj/6f3r640jv9E5+yTXIZOWsxk0= +github.com/vrischmann/envconfig v1.2.0/go.mod h1:c5DuUlkzfsnspy1g7qiqryPCsW+NjsrLsYq4zhwsoHo= +golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67 h1:ng3VDlRp5/DHpSWl02R4rM9I+8M2rhmsuLwAMmkLQWE= +golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/main.go b/main.go new file mode 100644 index 0000000..54941ab --- /dev/null +++ b/main.go @@ -0,0 +1,24 @@ +package main + +import ( + // stdlib + "log" + "os" + + // local + "go.dev.pztrn.name/discordrone/env" + "go.dev.pztrn.name/discordrone/message" +) + +func main() { + env.ParseEnv() + if env.Data.Debug { + log.Printf("Starting Discordrone with parameters: %+v\n", env.Data) + } + + err := message.Process() + if err != nil { + log.Fatalln("Error appeared:", err) + os.Exit(1) + } +} diff --git a/message/payload.go b/message/payload.go new file mode 100644 index 0000000..b13dad5 --- /dev/null +++ b/message/payload.go @@ -0,0 +1,43 @@ +package message + +type ( + // Payload that will be sent to Discord. + payload struct { + Wait bool `json:"wait"` + Content string `json:"content"` + Username string `json:"username"` + AvatarURL string `json:"avatar_url"` + TTS bool `json:"tts"` + Embeds []EmbedObject `json:"embeds"` + } + + // EmbedFooterObject for Embed Footer Structure. + EmbedFooterObject struct { + Text string `json:"text"` + IconURL string `json:"icon_url"` + } + + // EmbedAuthorObject for Embed Author Structure + EmbedAuthorObject struct { + Name string `json:"name"` + URL string `json:"url"` + IconURL string `json:"icon_url"` + } + + // EmbedFieldObject for Embed Field Structure + EmbedFieldObject struct { + Name string `json:"name"` + Value string `json:"value"` + } + + // EmbedObject is for Embed Structure + EmbedObject struct { + Title string `json:"title"` + Description string `json:"description"` + URL string `json:"url"` + Color int `json:"color"` + Footer EmbedFooterObject `json:"footer"` + Author EmbedAuthorObject `json:"author"` + Fields []EmbedFieldObject `json:"fields"` + } +) diff --git a/message/prepare.go b/message/prepare.go new file mode 100644 index 0000000..19f2127 --- /dev/null +++ b/message/prepare.go @@ -0,0 +1,8 @@ +package message + +// stdlib + +// local + +// other +//"github.com/drone/drone-template-lib/template" diff --git a/message/process.go b/message/process.go new file mode 100644 index 0000000..bd67dc9 --- /dev/null +++ b/message/process.go @@ -0,0 +1,55 @@ +package message + +import ( + // stdlib + "errors" + "log" + + // local + "go.dev.pztrn.name/discordrone/env" + + // other + "github.com/drone/drone-template-lib/template" +) + +// Process starts message(s) processing. +func Process() error { + if env.Data.Debug { + log.Println("Preparing message for sending...") + } + + // Webhook data should present. + if env.Data.Plugin.Webhook.ID == "" && env.Data.Plugin.Webhook.Token == "" { + return errors.New("webhook id or token is missing") + } + + // Payload. + p := &payload{ + Embeds: []EmbedObject{ + createEmbed(), + }, + } + + // If no additional message was passed - send what we got. + if len(env.Data.Plugin.Message) == 0 { + err := sendMessage(p) + if err != nil { + return err + } + } + + // If message was set - format it. + if len(env.Data.Plugin.Message) > 0 { + text, err := template.RenderTrim(env.Data.Plugin.Message, env.Data) + if err != nil { + return err + } + p.Content = text + err1 := sendMessage(p) + if err1 != nil { + return err1 + } + } + + return nil +} diff --git a/message/send_message.go b/message/send_message.go new file mode 100644 index 0000000..a9a1813 --- /dev/null +++ b/message/send_message.go @@ -0,0 +1,28 @@ +package message + +import ( + // stdlib + "bytes" + "encoding/json" + "fmt" + "net/http" + + // local + "go.dev.pztrn.name/discordrone/env" +) + +// Sends message to Discord. +func sendMessage(message interface{}) error { + webhookURL := fmt.Sprintf("https://discordapp.com/api/webhooks/%s/%s", env.Data.Plugin.Webhook.ID, env.Data.Plugin.Webhook.Token) + b := new(bytes.Buffer) + if err := json.NewEncoder(b).Encode(message); err != nil { + return err + } + _, err := http.Post(webhookURL, "application/json; charset=utf-8", b) + + if err != nil { + return err + } + + return nil +} diff --git a/message/template.go b/message/template.go new file mode 100644 index 0000000..5ea2a70 --- /dev/null +++ b/message/template.go @@ -0,0 +1,79 @@ +package message + +import ( + // stdlib + "strconv" + "strings" + + // local + "go.dev.pztrn.name/discordrone/env" +) + +const ( + // DroneIconURL default drone logo url + droneIconURL = "https://c1.staticflickr.com/5/4236/34957940160_435d83114f_z.jpg" + // DroneDesc default drone description + droneDesc = "Powered by DiscoDrone Plugin" +) + +func createEmbed() EmbedObject { + // Create initial payload object. + // Description. + // Color. + embed := EmbedObject{ + Title: env.Data.Drone.Commit.Message, + URL: env.Data.Drone.Build.Link, + Author: EmbedAuthorObject{ + Name: env.Data.Drone.Commit.Author.Name, + IconURL: env.Data.Drone.Commit.Author.Avatar, + }, + Footer: EmbedFooterObject{ + Text: droneDesc, + IconURL: droneIconURL, + }, + } + + // Compose description. + var description string + // ToDo: promote/rollback? + switch env.Data.Drone.Build.Event { + case "pull_request": + var branch string + if env.Data.Drone.Commit.Ref != "" { + branch = env.Data.Drone.Commit.Ref + } else { + branch = env.Data.Drone.Commit.Branch + } + description = env.Data.Drone.Commit.Author.Name + " updated pull request " + branch + case "push": + description = env.Data.Drone.Commit.Author.Name + " pushed to " + env.Data.Drone.Commit.Branch + case "tag": + description = env.Data.Drone.Commit.Author.Name + " pushed tag " + env.Data.Drone.Commit.Branch + } + + embed.Description = description + + // Compose color. + var color int + if env.Data.Plugin.Color != "" { + env.Data.Plugin.Color = strings.Replace(env.Data.Plugin.Color, "#", "", -1) + if s, err := strconv.ParseInt(env.Data.Plugin.Color, 16, 32); err == nil { + color = int(s) + } + } + + switch env.Data.Drone.Build.Status { + case "success": + // green + color = 0x1ac600 + case "failure", "error", "killed": + // red + color = 0xff3232 + default: + // yellow + color = 0xffd930 + } + embed.Color = color + + return embed +}