diff --git a/.drone.yml b/.drone.yml index ea54770..7f0e349 100644 --- a/.drone.yml +++ b/.drone.yml @@ -21,13 +21,19 @@ steps: - master status: - success - event: - - push settings: repo: lddsb/drone-dingtalk-message dockerfile: Dockerfile - tags: latest + tags: + - latest + - 1.0.0 username: from_secret: docker_username password: from_secret: docker_password + +trigger: + branch: + - master + event: + - tags diff --git a/Gopkg.lock b/Gopkg.lock index 5896f27..0a0ee9a 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -12,6 +12,14 @@ revision = "23d116af351c84513e1946b527c88823e476be13" version = "v1.3.0" +[[projects]] + branch = "master" + digest = "1:9142979c770f3d0f3c42c2eec532048bbbe2571134da91e8946cd8610c85c04b" + name = "github.com/lddsb/dingtalk-webhook" + packages = ["."] + pruneopts = "UT" + revision = "b4abe34b5fa9af8ea7d5f28c02bd314558b21f7f" + [[projects]] digest = "1:b24d38b282bacf9791408a080f606370efa3d364e4b5fd9ba0f7b87786d3b679" name = "github.com/urfave/cli" @@ -25,6 +33,7 @@ analyzer-version = 1 input-imports = [ "github.com/joho/godotenv/autoload", + "github.com/lddsb/dingtalk-webhook", "github.com/urfave/cli", ] solver-name = "gps-cdcl" diff --git a/Gopkg.toml b/Gopkg.toml index 744b8fa..857f0f2 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -36,3 +36,7 @@ [prune] go-tests = true unused-packages = true + +[[constraint]] + branch = "master" + name = "github.com/lddsb/dingtalk-webhook" diff --git a/main.go b/main.go index ff736e2..09fdf91 100644 --- a/main.go +++ b/main.go @@ -148,35 +148,36 @@ func main() { if err := app.Run(os.Args); nil != err { log.Println(err) - os.Exit(1) } } // run with args func run(c *cli.Context) { plugin := Plugin{ - // repo info - Repo: Repo{ - FullName: c.String("repo.fullname"), - }, - // build info - Build: Build{ - Status: c.String("build.status"), - Link: c.String("build.link"), - }, - Commit: Commit{ - Sha: c.String("commit.sha"), - Branch: c.String("commit.branch"), - Message: c.String("commit.message"), - Link: c.String("commit.link"), - Authors: struct { - Avatar string - Email string - Name string - }{ - Avatar: c.String("commit.author.avatar"), - Email: c.String("commit.author.email"), - Name: c.String("commit.author.name"), + Drone: Drone{ + // repo info + Repo: Repo{ + FullName: c.String("repo.fullname"), + }, + // build info + Build: Build{ + Status: c.String("build.status"), + Link: c.String("build.link"), + }, + Commit: Commit{ + Sha: c.String("commit.sha"), + Branch: c.String("commit.branch"), + Message: c.String("commit.message"), + Link: c.String("commit.link"), + Authors: struct { + Avatar string + Email string + Name string + }{ + Avatar: c.String("commit.author.avatar"), + Email: c.String("commit.author.email"), + Name: c.String("commit.author.name"), + }, }, }, // custom config @@ -189,18 +190,21 @@ func run(c *cli.Context) { Debug: c.Bool("config.debug"), }, Extra: Extra{ - SuccessPicUrl: c.String("config.success.pic.url"), - FailurePicUrl: c.String("config.failure.pic.url"), - SuccessColor: c.String("config.success.color"), - FailureColor: c.String("config.failure.color"), - WithColor: c.Bool("config.message.color"), - WithPic: c.Bool("config.message.pic"), - LinkSha: c.Bool("config.message.sha.link"), + Pic: ExtraPic{ + WithPic: c.Bool("config.message.pic"), + SuccessPicURL: c.String("config.success.pic.url"), + FailurePicURL: c.String("config.failure.pic.url"), + }, + Color: ExtraColor{ + SuccessColor: c.String("config.success.color"), + FailureColor: c.String("config.failure.color"), + WithColor: c.Bool("config.message.color"), + }, + LinkSha: c.Bool("config.message.sha.link"), }, } if err := plugin.Exec(); nil != err { fmt.Println(err) - os.Exit(1) } } diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..d884907 --- /dev/null +++ b/main_test.go @@ -0,0 +1,10 @@ +package main + +import ( + "testing" +) + +func TestMain(t *testing.T) { + main() + t.Log("main testing finished") +} diff --git a/plugin.go b/plugin.go index 701fb0a..a888d87 100644 --- a/plugin.go +++ b/plugin.go @@ -5,6 +5,8 @@ import ( "fmt" "log" "strings" + + webhook "github.com/lddsb/dingtalk-webhook" ) type ( @@ -12,58 +14,85 @@ type ( Repo struct { FullName string // repository full name } + // Build `build info` Build struct { Status string // providers the current build status Link string // providers the current build link } + // Commit `commit info` Commit struct { Branch string // providers the branch for the current commit Link string // providers the http link to the current commit in the remote source code management system(e.g.GitHub) Message string // providers the commit message for the current build Sha string // providers the commit sha for the current build - // repo author info - Authors struct { - Avatar string // providers the author avatar for the current commit - Email string // providers the author email for the current commit - Name string // providers the author name for the current commit - } + Authors CommitAuthors } + + // CommitAuthors `commit author info` + CommitAuthors struct { + Avatar string // providers the author avatar for the current commit + Email string // providers the author email for the current commit + Name string // providers the author name for the current commit + } + + // Drone `drone info` + Drone struct { + Repo Repo + Build Build + Commit Commit + } + // Config `plugin private config` Config struct { - Debug bool - AccessToken string - IsAtALL bool - Mobiles string - Username string - MsgType string + Debug bool + AccessToken string + IsAtALL bool + Mobiles string + Username string + MsgType string + } + + // MessageConfig `DingTalk message struct` + MessageConfig struct { + ActionCard ActionCard + } + + // ActionCard `action card message struct` + ActionCard struct { LinkUrls string LinkTitles string HideAvatar bool BtnOrientation bool } + // Extra `extra variables` Extra struct { - PicURL string - MsgURL string - SuccessPicUrl string - FailurePicUrl string - SuccessColor string - FailureColor string - WithColor bool + Color ExtraColor + Pic ExtraPic + LinkSha bool + } + + // ExtraPic `extra config for pic` + ExtraPic struct { WithPic bool - LinkSha bool + SuccessPicURL string + FailurePicURL string + } + + // ExtraColor `extra config for color` + ExtraColor struct { + WithColor bool + SuccessColor string + FailureColor string } // Plugin `plugin all config` Plugin struct { - Commit Commit - Repo Repo - Build Build - Config Config - Extra Extra - WebHook *WebHook + Drone Drone + Config Config + Extra Extra } ) @@ -74,25 +103,30 @@ func (p *Plugin) Exec() error { msg := "missing dingtalk access token" return errors.New(msg) } - p.WebHook = NewWebHook(p.Config.AccessToken) + + if 6 > len(p.Drone.Commit.Sha) { + return errors.New("commit sha cannot short than 6") + } + + newWebhook := webhook.NewWebHook(p.Config.AccessToken) mobiles := strings.Split(p.Config.Mobiles, ",") switch strings.ToLower(p.Config.MsgType) { case "markdown": - err = p.WebHook.SendMarkdownMsg("You have a new message...", p.baseTpl(), p.Config.IsAtALL, mobiles...) + err = newWebhook.SendMarkdownMsg("You have a new message...", p.baseTpl(), p.Config.IsAtALL, mobiles...) case "text": - err = p.WebHook.SendTextMsg(p.baseTpl(), p.Config.IsAtALL, mobiles...) + err = newWebhook.SendTextMsg(p.baseTpl(), p.Config.IsAtALL, mobiles...) case "link": - err = p.WebHook.SendLinkMsg(p.Build.Status, p.baseTpl(), p.Commit.Authors.Avatar, p.Build.Link) + err = newWebhook.SendLinkMsg(p.Drone.Build.Status, p.baseTpl(), p.Drone.Commit.Authors.Avatar, p.Drone.Build.Link) default: msg := "not support message type" err = errors.New(msg) } - if err != nil { - return err + if err == nil { + log.Println("send message success!") } - log.Println("send message success!") - return nil + + return err } // markdownTpl `output the tpl of markdown` @@ -101,44 +135,44 @@ func (p *Plugin) markdownTpl() string { // title title := fmt.Sprintf(" %s *Branch Build %s*", - strings.Title(p.Commit.Branch), - strings.Title(p.Build.Status)) + strings.Title(p.Drone.Commit.Branch), + strings.Title(p.Drone.Build.Status)) // with color on title - if p.Extra.WithColor { + if p.Extra.Color.WithColor { title = fmt.Sprintf("%s", p.getColor(), title) } tpl = fmt.Sprintf("# %s \n", title) // with pic - if p.Extra.WithPic { + if p.Extra.Pic.WithPic { tpl += fmt.Sprintf("![%s](%s)\n\n", - p.Build.Status, - p.getPicUrl()) + p.Drone.Build.Status, + p.getPicURL()) } // commit message - commitMsg := fmt.Sprintf("%s", p.Commit.Message) - if p.Extra.WithColor { + commitMsg := fmt.Sprintf("%s", p.Drone.Commit.Message) + if p.Extra.Color.WithColor { commitMsg = fmt.Sprintf("%s", p.getColor(), commitMsg) } tpl += commitMsg + "\n\n" // sha info - commitSha := p.Commit.Sha + commitSha := p.Drone.Commit.Sha if p.Extra.LinkSha { - commitSha = fmt.Sprintf("[Click To %s Commit Detail Page](%s)", commitSha[:6], p.Commit.Link) + commitSha = fmt.Sprintf("[Click To %s Commit Detail Page](%s)", commitSha[:6], p.Drone.Commit.Link) } tpl += commitSha + "\n\n" // author info - authorInfo := fmt.Sprintf("`%s(%s)`", p.Commit.Authors.Name, p.Commit.Authors.Email) + authorInfo := fmt.Sprintf("`%s(%s)`", p.Drone.Commit.Authors.Name, p.Drone.Commit.Authors.Email) tpl += authorInfo + "\n\n" // build detail link buildDetail := fmt.Sprintf("[Click To The Build Detail Page %s](%s)", p.getEmoticon(), - p.Build.Link) + p.Drone.Build.Link) tpl += buildDetail return tpl } @@ -154,20 +188,20 @@ func (p *Plugin) baseTpl() string { @%s %s (%s) `, - p.Build.Status, - strings.TrimSpace(p.Commit.Message), - p.Repo.FullName, - p.Commit.Branch, - p.Commit.Sha, - p.Commit.Authors.Name, - p.Commit.Authors.Email) + p.Drone.Build.Status, + strings.TrimSpace(p.Drone.Commit.Message), + p.Drone.Repo.FullName, + p.Drone.Commit.Branch, + p.Drone.Commit.Sha, + p.Drone.Commit.Authors.Name, + p.Drone.Commit.Authors.Email) case "link": tpl = fmt.Sprintf(`%s(%s) @%s %s(%s)`, - p.Repo.FullName, - p.Commit.Branch, - p.Commit.Sha[:6], - p.Commit.Authors.Name, - p.Commit.Authors.Email) + p.Drone.Repo.FullName, + p.Drone.Commit.Branch, + p.Drone.Commit.Sha[:6], + p.Drone.Commit.Authors.Name, + p.Drone.Commit.Authors.Email) case "actionCard": // coming soon @@ -184,7 +218,7 @@ func (p *Plugin) getEmoticon() string { emoticons["success"] = ":)" emoticons["failure"] = ":(" - emoticon, ok := emoticons[p.Build.Status] + emoticon, ok := emoticons[p.Drone.Build.Status] if ok { return emoticon } @@ -195,20 +229,20 @@ func (p *Plugin) getEmoticon() string { /** get picture url */ -func (p *Plugin) getPicUrl() string { +func (p *Plugin) getPicURL() string { pics := make(map[string]string) // success picture url pics["success"] = "https://ws4.sinaimg.cn/large/006tNc79gy1fz05g5a7utj30he0bfjry.jpg" - if p.Extra.SuccessPicUrl != "" { - pics["success"] = p.Extra.SuccessPicUrl + if p.Extra.Pic.SuccessPicURL != "" { + pics["success"] = p.Extra.Pic.SuccessPicURL } // failure picture url pics["failure"] = "https://ws1.sinaimg.cn/large/006tNc79gy1fz0b4fghpnj30hd0bdmxn.jpg" - if p.Extra.FailurePicUrl != "" { - pics["failure"] = p.Extra.FailurePicUrl + if p.Extra.Pic.FailurePicURL != "" { + pics["failure"] = p.Extra.Pic.FailurePicURL } - url, ok := pics[p.Build.Status] + url, ok := pics[p.Drone.Build.Status] if ok { return url } @@ -223,16 +257,16 @@ func (p *Plugin) getColor() string { colors := make(map[string]string) // success color colors["success"] = "#008000" - if p.Extra.SuccessColor != "" { - colors["success"] = "#" + p.Extra.SuccessColor + if p.Extra.Color.SuccessColor != "" { + colors["success"] = "#" + p.Extra.Color.SuccessColor } // failure color colors["failure"] = "#FF0000" - if p.Extra.FailureColor != "" { - colors["failure"] = "#" + p.Extra.FailureColor + if p.Extra.Color.FailureColor != "" { + colors["failure"] = "#" + p.Extra.Color.FailureColor } - color, ok := colors[p.Build.Status] + color, ok := colors[p.Drone.Build.Status] if ok { return color } diff --git a/plugin_test.go b/plugin_test.go new file mode 100644 index 0000000..5ea7675 --- /dev/null +++ b/plugin_test.go @@ -0,0 +1,59 @@ +package main + +import ( + "testing" +) + +func TestPlugin(t *testing.T) { + p := Plugin{} + err := p.Exec() + if nil == err { + t.Error("access token empty error should be catch!") + } + + p.Config.AccessToken = "example-access-token" + err = p.Exec() + if nil == err { + t.Error("commit sha length error should be catch!") + } + + p.Drone.Commit.Sha = "53729847dfksj" + err = p.Exec() + if nil == err { + t.Error("not support message type error should be catch!") + } + + p.Config.MsgType = "text" + err = p.Exec() + if nil == err { + t.Error("access token invalid error should be catch!") + } + + p.Config.MsgType = "link" + err = p.Exec() + if nil == err { + t.Error("access token invalid error should be catch!") + } + + p.Extra.Color.WithColor = true + p.Extra.Color.FailureColor = "#555555" + p.Extra.Color.SuccessColor = "#222222" + p.Extra.Pic.WithPic = true + p.Extra.Pic.FailurePicURL = "https://www.baidu.com" + p.Extra.Pic.SuccessPicURL = "https://www.baidu.com" + p.Extra.LinkSha = true + // p.Drone.Build.Status = "failure" + p.Config.MsgType = "markdown" + err = p.Exec() + if nil == err { + t.Error("access token invalid error should be catch!") + } + + p.Drone.Build.Status = "failure" + err = p.Exec() + if nil == err { + t.Error("access token invalid error should be catch!") + } + + t.Log("plugin testing finished") +} diff --git a/webhook.go b/webhook.go deleted file mode 100644 index 8ecfa09..0000000 --- a/webhook.go +++ /dev/null @@ -1,243 +0,0 @@ -package main - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "io/ioutil" - "net/http" - "regexp" -) - -// LinkMsg `link message struct` -type LinkMsg struct { - Title string `json:"title"` - MessageURL string `json:"messageURL"` - PicURL string `json:"picURL"` -} - -// ActionCard `action card message struct` -type ActionCard struct { - Text string `json:"text"` - Title string `json:"title"` - SingleTitle string `json:"singleTitle"` - SingleURL string `json:"singleURL"` - BtnOrientation string `json:"btnOrientation"` - HideAvatar string `json:"hideAvatar"` // robot message avatar - Buttons []struct { - Title string `json:"title"` - ActionURL string `json:"actionURL"` - } `json:"btns"` -} - -// PayLoad payload -type PayLoad struct { - MsgType string `json:"msgtype"` - Text struct { - Content string `json:"content"` - } `json:"text"` - Link struct { - Title string `json:"title"` - Text string `json:"text"` - PicUrl string `json:"picUrl"` - MessageUrl string `json:"messageUrl"` - } `json:"link"` - Markdown struct { - Title string `json:"title"` - Text string `json:"text"` - } `json:"markdown"` - ActionCard ActionCard `json:"actionCard"` - FeedCard struct { - Links []LinkMsg `json:"links"` - } `json:"feedCard"` - At struct { - AtMobiles []string `json:"atMobiles"` - IsAtAll bool `json:"isAtAll"` - } `json:"at"` -} - -// WebHook `web hook base config` -type WebHook struct { - AccessToken string `json:"accessToken"` -} - -// NewWebHook `new a webhook` -func NewWebHook(accessToken string) *WebHook { - return &WebHook{AccessToken: accessToken} -} - -// Response `dingtalk webhook response struct` -type Response struct { - ErrorCode int `json:"errcode"` - ErrorMessage string `json:"errmsg"` -} - -var baseApi = "https://oapi.dingtalk.com/robot/send?access_token=" -var reg = `^1([38][0-9]|14[57]|5[^4])\d{8}$` -var regx = regexp.MustCompile(reg) - -// real send request to api -func (w *WebHook) sendPayload(payload *PayLoad) error { - // get config - bs, err := json.Marshal(payload) - if nil != err { - return err - } - // request api - resp, err := http.Post(baseApi+w.AccessToken, "application/json", bytes.NewReader(bs)) - if nil != err { - return err - } - // read response body - body, err := ioutil.ReadAll(resp.Body) - if nil != err { - return err - } - // api unusual - if 200 != resp.StatusCode { - return fmt.Errorf("%d: %s", resp.StatusCode, string(body)) - } - - var result Response - // json decode - err = json.Unmarshal(body, &result) - if nil != err { - return err - } - if 0 != result.ErrorCode { - return fmt.Errorf("%d: %s", result.ErrorCode, result.ErrorMessage) - } - - return nil -} - -// SendTextMsg `send a text message` -func (w *WebHook) SendTextMsg(content string, isAtAll bool, mobiles ...string) error { - // send request - return w.sendPayload(&PayLoad{ - MsgType: "text", - Text: struct { - Content string `json:"content"` - }{ - Content: content, - }, - At: struct { - AtMobiles []string `json:"atMobiles"` - IsAtAll bool `json:"isAtAll"` - }{ - AtMobiles: mobiles, - IsAtAll: isAtAll, - }, - }) -} - -// SendLinkMsg `send a link message` -func (w *WebHook) SendLinkMsg(title, content, picURL, msgURL string) error { - return w.sendPayload(&PayLoad{ - MsgType: "link", - Link: struct { - Title string `json:"title"` - Text string `json:"text"` - PicUrl string `json:"picUrl"` - MessageUrl string `json:"messageUrl"` - }{ - Title: title, - Text: content, - PicUrl: picURL, - MessageUrl: msgURL, - }, - }) -} - -// SendMarkdownMsg `send a markdown msg` -func (w *WebHook) SendMarkdownMsg(title, content string, isAtAll bool, mobiles ...string) error { - firstLine := false - for _, mobile := range mobiles { - if regx.MatchString(mobile) { - if false == firstLine { - content += "#####" - } - content += " @" + mobile - firstLine = true - } - } - // send request - return w.sendPayload(&PayLoad{ - MsgType: "markdown", - Markdown: struct { - Title string `json:"title"` - Text string `json:"text"` - }{ - Title: title, - Text: content, - }, - At: struct { - AtMobiles []string `json:"atMobiles"` - IsAtAll bool `json:"isAtAll"` - }{ - AtMobiles: mobiles, - IsAtAll: isAtAll, - }, - }) -} - -// SendActionCardMsg `send single action card message` -func (w *WebHook) SendActionCardMsg(title, content string, linkTitles, linkUrls []string, hideAvatar, btnOrientation bool) error { - // validation is empty - if 0 == len(linkTitles) || 0 == len(linkUrls) { - return errors.New("links or titles is empty!") - } - // validation is equal - if len(linkUrls) != len(linkTitles) { - return errors.New("links length and titles length is not equal!") - } - // hide robot avatar - var strHideAvatar = "0" - if hideAvatar { - strHideAvatar = "1" - } - // button sort - var strBtnOrientation = "0" - if btnOrientation { - strBtnOrientation = "1" - } - // button struct - var buttons []struct { - Title string `json:"title"` - ActionURL string `json:"actionURL"` - } - // inject to button - for i := 0; i < len(linkTitles); i++ { - buttons = append(buttons, struct { - Title string `json:"title"` - ActionURL string `json:"actionURL"` - }{ - Title: linkTitles[i], - ActionURL: linkUrls[i], - }) - } - // send request - return w.sendPayload(&PayLoad{ - MsgType: "actionCard", - ActionCard: ActionCard{ - Title: title, - Text: content, - HideAvatar: strHideAvatar, - BtnOrientation: strBtnOrientation, - Buttons: buttons, - }, - }) -} - -// SendLinkCardMsg `send link card message` -func (w *WebHook) SendLinkCardMsg(messages []LinkMsg) error { - return w.sendPayload(&PayLoad{ - MsgType: "feedCard", - FeedCard: struct { - Links []LinkMsg `json:"links"` - }{ - Links: messages, - }, - }) -}