complete the first version

This commit is contained in:
lddsb
2018-12-02 22:40:50 +08:00
commit 22302af994
6 changed files with 844 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
*
!.gitignore
!main.go
!plugin.go
!webhook.go
!README.md
!Dockerfile
+9
View File
@@ -0,0 +1,9 @@
FROM alpine:latest
RUN apk update && \
apk add \
ca-certificates && \
rm -rf /var/cache/apk/*
ADD drone-dingtalk-message /bin/
ENTRYPOINT ["/bin/drone-dingtalk-message"]
+39
View File
@@ -0,0 +1,39 @@
# Drone CI DingTalk Message Plugin
### Development
> First get this repo
```shell
go get github.com/lddsb/drone-dingtalk-message
```
> get dependent lib
```shell
go get github.com/urfave/cli
```
> build
```shell
cd $GOPATH/src/github.com/lddsb/drone-dingtalk-message && go build .
```
> run
```shell
./drone-dingtalk-notification -h
```
### Drone CI Plugin Config
```yaml
pipeline:
# other step here
message:
image: lddsb/drone-dingtalk-message
environment:
- PLUGIN_ACCESS_TOKEN=xxx
- PLUGIN_MSG_TYPE=markdown
```
### Drone CI Plugin Configs
|ENV|description|is require|
|:-:|:-:|:-:|:-:|
|PLUGIN_ACCESS_TOKEN|dingtalk access token|Yes|
|PLUGIN_MSG_TYPE|dingtalk message type, optional: text, markdown, link, actionCard, feedCard|Yes|
|PLUGIN_MSG_AT_ALL|dingtalk message at all in a group by robot(`default close`)|No|
|PLUGIN_MSG_AT_MOBILES|dingtalk message at anyone in a group by mobiles(`default empty`)|No|
|PLUGIN_LANG|dingtalk message lang, optional: zh_CN and en_US(`default zh_CN`)|No|
+349
View File
@@ -0,0 +1,349 @@
package main
import (
"fmt"
"log"
"os"
_ "github.com/joho/godotenv/autoload"
"github.com/urfave/cli"
)
var Version = "0.1.1202"
func main() {
app := cli.NewApp()
app.Name = "Drone Dingtalk Message Plugin"
app.Usage = "Sending message to Dingtalk group by robot using webhook"
app.Copyright = "© 2018 Dee Luo"
app.Authors = []cli.Author{
{
Name: "Dee Luo",
Email: "luodi0128@gmail.com",
},
}
app.Action = run
app.Version = Version
app.Flags = []cli.Flag{
cli.StringFlag{
Name: "config.token",
Usage: "dingtalk webhook access token",
EnvVar: "PLUGIN_ACCESS_TOKEN",
},
cli.StringFlag{
Name: "config.lang",
Value: "zh_CN",
Usage: "the lang display (zh_CN or en_US, zh_CN is default)",
EnvVar: "PLUGIN_LANG",
},
cli.StringFlag{
Name: "config.message.type",
Usage: "dingtalk message type, like text, markdown, action card, link and feed card...",
EnvVar: "PLUGIN_MSG_TYPE",
},
cli.StringFlag{
Name: "config.message.at.all",
Usage: "at all in a message(only text and markdown type message can at)",
EnvVar: "PLUGIN_MSG_AT_ALL",
},
cli.StringFlag{
Name: "config.message.at.mobiles",
Usage: "at someone in a dingtalk group need this guy bind's mobile",
EnvVar: "PLUGIN_MSG_AT_MOBILES",
},
cli.BoolFlag{
Name: "drone",
Usage: "indicates the runtime environment is Drone",
EnvVar: "DRONE",
},
cli.StringFlag{
Name: "branch",
Usage: "providers the branch for the current build",
EnvVar: "DRONE_BRANCH",
},
// commit args start
cli.StringFlag{
Name: "remote.url",
Usage: "git remote url",
EnvVar: "DRONE_REMOTE_URL",
},
cli.StringFlag{
Name: "commit.author.avatar",
Usage: "providers the author avatar url for the current commit",
EnvVar: "DRONE_COMMIT_AUTHOR_AVATAR",
},
cli.StringFlag{
Name: "commit.author.email",
Usage: "providers the author email for the current commit",
EnvVar: "DRONE_COMMIT_AUTHOR_EMAIL",
},
cli.StringFlag{
Name: "commit.author.name",
Usage: "providers the author name for the current commit",
EnvVar: "DRONE_COMMIT_AUTHOR",
},
cli.StringFlag{
Name: "commit.branch",
Usage: "providers the branch for the current build",
EnvVar: "DRONE_COMMIT_BRANCH",
Value: "master",
},
cli.StringFlag{
Name: "commit.link",
Usage: "providers the http link to the current commit in the remote source code management system(e.g.GitHub)",
EnvVar: "DRONE_COMMIT_LINK",
},
cli.StringFlag{
Name: "commit.message",
Usage: "providers the commit message for the current build",
EnvVar: "DRONE_COMMIT_MESSAGE",
},
cli.StringFlag{
Name: "commit.ref",
Usage: "providers the reference for the current build",
EnvVar: "DRONE_COMMIT_REF",
},
cli.StringFlag{
Name: "commit.sha",
Usage: "providers the commit sha for the current build",
EnvVar: "DRONE_COMMIT_SHA",
},
// commit args end
cli.StringFlag{
Name: "git.url.http",
Usage: "providers the repository git+http url",
EnvVar: "DRONE_GIT_HTTP_URL",
},
cli.StringFlag{
Name: "git.url.ssh",
Usage: "providers the repository git+ssh url",
EnvVar: "DRONE_GIT_SSH_URL",
},
cli.StringFlag{
Name: "machine",
Usage: "providers the Drone agent hostname",
EnvVar: "DRONE_MACHINE",
},
cli.StringFlag{
Name: "pull.request",
Usage: "providers the pull request number for the current build.This value is only set if the build event is of type pull request",
EnvVar: "DRONE_PULL_REQUEST",
},
// repo args start
cli.StringFlag{
Name: "repo.fullname",
Usage: "providers the full name of the repository",
EnvVar: "DRONE_REPO",
},
cli.StringFlag{
Name: "repo.owner",
Usage: "repository owner",
EnvVar: "DRONE_REPO_OWNER",
},
cli.StringFlag{
Name: "repo.branch",
Usage: "providers the default repository branch(e.g.master)",
EnvVar: "DRONE_REPO_BRANCH",
},
cli.StringFlag{
Name: "repo.link",
Usage: "providers the repository http link",
EnvVar: "DRONE_REPO_LINK",
},
cli.StringFlag{
Name: "repo.name",
Usage: "providers the repository name",
EnvVar: "DRONE_REPO_NAME",
},
cli.StringFlag{
Name: "repo.avatar",
Usage: "repository avatar",
EnvVar: "DRONE_REPO_AVATAR",
},
cli.StringFlag{
Name: "repo.namespace",
Usage: "providers the repository namespace(e.g. account owner)",
EnvVar: "DRONE_REPO_NAMESPACE",
},
cli.BoolFlag{
Name: "repo.private",
Usage: "indicates the repository is public or private",
EnvVar: "DRONE_REPO_PRIVATE",
},
cli.BoolFlag{
Name: "repo.trusted",
Usage: "repository is trusted",
EnvVar: "DRONE_REPO_TRUSTED",
},
// repo args end
cli.StringFlag{
Name: "runner.host",
Usage: "provider are Drone agent hostname",
EnvVar: "DRONE_RUNNER_HOST",
},
cli.StringFlag{
Name: "runner.hostname",
Usage: "providers the Drone agent hostname",
EnvVar: "DRONE_RUNNER_HOSTNAME",
},
cli.StringFlag{
Name: "runner.platform",
Usage: "providers the Drone agent os and architecture",
EnvVar: "DRONE_RUNNER_PLATFORM",
},
cli.StringFlag{
Name: "runner.label",
Usage: "404 not found",
EnvVar: "DRONE_RUNNER_LABEL",
},
cli.StringFlag{
Name: "source.branch",
Usage: "providers the source branch for a pull request",
EnvVar: "DRONE_SOURCE_BRANCH",
},
cli.StringFlag{
Name: "target.branch",
Usage: "providers the target branch for a pull request",
EnvVar: "DRONE_TARGET_BRANCH",
},
cli.StringFlag{
Name: "system.host",
Usage: "providers the Drone server hostname",
EnvVar: "DRONE_SYSTEM_HOST",
},
cli.StringFlag{
Name: "system.hostname",
Usage: "providers the Drone server hostname",
EnvVar: "DRONE_SYSTEM_HOSTNAME",
},
cli.StringFlag{
Name: "system.version",
Usage: "providers the Drone server version",
EnvVar: "DRONE_SYSTEM_VERSION",
},
cli.StringFlag{
Name: "tag",
Usage: "providers the tag name for the current build.This value is only set if the build event is of type tag",
EnvVar: "DRONE_TAG",
},
// build args start
cli.StringFlag{
Name: "build.event",
Value: "push",
Usage: "build event",
EnvVar: "DRONE_BUILD_EVENT",
},
cli.IntFlag{
Name: "build.number",
Usage: "build number",
EnvVar: "DRONE_BUILD_NUMBER",
},
cli.IntFlag{
Name: "build.created",
Usage: "build created",
EnvVar: "DRONE_BUILD_CREATED",
},
cli.IntFlag{
Name: "build.started",
Usage: "build started",
EnvVar: "DRONE_BUILD_STARTED",
},
cli.IntFlag{
Name: "build.finished",
Usage: "build finished",
EnvVar: "DRONE_BUILD_FINISHED",
},
cli.StringFlag{
Name: "build.status",
Usage: "build status",
Value: "success",
EnvVar: "DRONE_BUILD_STATUS",
},
cli.StringFlag{
Name: "build.link",
Usage: "build link",
EnvVar: "DRONE_BUILD_LINK",
},
cli.StringFlag{
Name: "build.deploy",
Usage: "build deployment target",
EnvVar: "DRONE_DEPLOY_TO",
},
cli.BoolFlag{
Name: "yaml.verified",
Usage: "build yaml is verified",
EnvVar: "DRONE_YAML_VERIFIED",
},
cli.BoolFlag{
Name: "yaml.signed",
Usage: "build yaml is signed",
EnvVar: "DRONE_YAML_SIGNED",
},
// build args end
cli.Float64Flag{
Name: "job.started",
Usage: "job started",
EnvVar: "DRONE_JOB_STARTED",
},
cli.Float64Flag{
Name: "job.finished",
Usage: "job finished",
EnvVar: "DRONE_JOB_FINISHED",
},
}
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"),
Owner: c.String("repo.owner"),
Name: c.String("repo.name"),
},
// build info
Build: Build{
Action: c.String("build.action"),
Number: c.Int("build.number"),
Started: c.Float64("build.started"),
Created: c.Float64("build.created"),
Event: c.String("build.event"),
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
Config: Config{
AccessToken: c.String("config.token"),
Lang: c.String("config.lang"),
IsAtALL: c.Bool("config.message.at.all"),
MsgType: c.String("config.message.type"),
Mobiles: c.String("config.message.at.mobiles"),
},
}
if err := plugin.Exec(); nil != err {
fmt.Println(err)
os.Exit(1)
}
}
+199
View File
@@ -0,0 +1,199 @@
package main
import (
"errors"
"fmt"
"log"
"strings"
)
type (
// repo base info
Repo struct {
Owner string // providers the repository owner name
Name string // providers the repository name
Branch string // providers the default repository branch(e.g.master)
Link string // providers the repository http link
NameSpace string // providers the repository namespace(e.g.account owner)
Private bool // indicates the repository is public or private
Visibility string // providers the repository visibility level.Possible values are public,private and internal
SCM string // providers the repository version control system
FullName string // repository full name
}
// build info
Build struct {
Action string // document description not found
Created float64 // providers the date and time when the build was created in the system
Event string // providers the current build event
Number int // providers the current build number
Started float64 // providers the date and time when the build was started
Status string // providers the current build status
Link string // providers the current build link
}
// commit info
Commit struct {
After string // providers the commit sha for the current build
Author string // providers the author username for the current commit
Before string // providers the parent commit sha for the current build
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
Ref string // providers the reference 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
}
}
// git url info
Git struct {
HttpUrl string // providers the repository git+http url
SSHUrl string // providers the repository git+ssh url
}
// Drone runner info
Runner struct {
Host string // providers the Drone agent hostname
Hostname string // providers the Drone agent hostname
Platform string // providers the Drone agent os and architecture
Label string // document description not found
}
// Drone system info
System struct {
Host string // providers the Drone server hostname
Hostname string // providers the Drone server hostname
Version string // providers the Drone server version
}
// plugin private config
Config struct {
AccessToken string
Message string
Lang string
IsAtALL bool
Mobiles string
Username string
AvatarURL string
MsgType string
LinkUrls string
LinkTitles string
HideAvatar bool
BtnOrientation bool
PicURL string
MsgURL string
}
// plugin all config
Plugin struct {
Git Git
Runner Runner
System System
Commit Commit
Repo Repo
Build Build
Config Config
WebHook *WebHook
}
)
func (p *Plugin) Exec() error {
log.Println("start execute sending...")
if 0 == len(p.Config.AccessToken) {
msg := "missing dingtalk access token"
log.Println(msg)
return errors.New(msg)
}
log.Println("access token pass...")
p.WebHook = NewWebHook(p.Config.AccessToken)
mobiles := strings.Split(p.Config.Mobiles, ",")
linkUrls := strings.Split(p.Config.LinkUrls, ",")
linkTitles := strings.Split(p.Config.LinkTitles, ",")
log.Println("sending message type: " + p.Config.MsgType)
switch strings.ToLower(p.Config.MsgType) {
case "markdown":
err := p.WebHook.SendMarkdownMsg(
"You have a new message...",
p.baseTpl(),
p.Config.IsAtALL,
mobiles...
)
if nil != err {
log.Println(err)
return err
}
case "text":
err := p.WebHook.SendTextMsg(p.baseTpl(), p.Config.IsAtALL, mobiles...)
if nil != err {
log.Println(err)
return err
}
case "actioncard":
err := p.WebHook.SendActionCardMsg(
"A actionCard title",
p.baseTpl(),
linkUrls,
linkTitles,
p.Config.HideAvatar,
p.Config.BtnOrientation,
)
if nil != err {
log.Println(err)
return err
}
case "link":
err := p.WebHook.SendLinkMsg(p.Build.Status, p.baseTpl(), p.Commit.Authors.Avatar, p.Build.Link)
if nil != err {
log.Println(err)
return err
}
default:
msg := "not support message type"
log.Println(msg)
return errors.New(msg)
}
log.Println("send " + p.Config.MsgType + " message success!")
return nil
}
func (p *Plugin) baseTpl() string {
tpl := ""
switch strings.ToLower(p.Config.MsgType) {
case "markdown":
tpl = fmt.Sprintf(`# **%s**
### [%s](%s)
##### %s (%s)
##### @%s
##### %s(%s)
`,
strings.Title(p.Build.Status),
strings.TrimSpace(p.Commit.Message),
p.Build.Link,
p.Repo.FullName,
p.Commit.Branch,
p.Commit.Sha,
p.Commit.Authors.Name,
p.Commit.Authors.Email)
case "text":
tpl = fmt.Sprintf(`[%s] %s
%s (%s)
@%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)
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)
}
return tpl
}
+241
View File
@@ -0,0 +1,241 @@
package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"regexp"
)
// link message struct
type LinkMsg struct {
Title string `json:"title"`
MessageURL string `json:"messageURL"`
PicURL string `json:"picURL"`
}
// 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
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"`
}
// web hook base config
type WebHook struct {
AccessToken string `json:"accessToken"`
}
func NewWebHook(accessToken string) *WebHook {
return &WebHook{AccessToken: accessToken}
}
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
}
// 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,
},
})
}
// with 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,
},
})
}
// send 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,
},
})
}
// send single action card
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,
},
})
}
// 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,
},
})
}