├── .drone.yml ├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── image.yaml │ └── release.yaml ├── .gitignore ├── .goreleaser.yml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── README_ZH.md ├── go.mod ├── go.sum ├── main.go ├── plugin.go ├── plugin_test.go └── tpls ├── gitlab-ci.tpl ├── markdown.tpl ├── markdown_at_mobiles.tpl └── text.tpl /.drone.yml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: pipeline 3 | name: testing 4 | 5 | steps: 6 | - name: vet 7 | pull: always 8 | image: golang 9 | commands: 10 | - make vet 11 | environment: 12 | GO111MODULE: "on" 13 | volumes: 14 | - name: gopath 15 | path: /go 16 | 17 | - name: test 18 | pull: always 19 | image: golang 20 | commands: 21 | - make test 22 | - make coverage 23 | environment: 24 | GO111MODULE: "on" 25 | volumes: 26 | - name: gopath 27 | path: /go 28 | 29 | - name: codecov 30 | pull: always 31 | image: plugins/codecov 32 | settings: 33 | token: 34 | from_secret: codecov_token 35 | 36 | volumes: 37 | - name: gopath 38 | temp: {} 39 | 40 | trigger: 41 | event: 42 | - pull_request 43 | 44 | --- 45 | kind: pipeline 46 | name: dryrun 47 | 48 | steps: 49 | - name: build 50 | pull: always 51 | image: golang 52 | commands: 53 | - go build -a -o drone-dingtalk-message . 54 | environment: 55 | CGO_ENABLED: 0 56 | GO111MODULE: "on" 57 | 58 | - name: dryrun 59 | pull: always 60 | image: plugins/docker 61 | settings: 62 | cache_from: lddsb/drone-dingtalk-message 63 | dockerfile: Dockerfile 64 | dry_run: true 65 | repo: lddsb/drone-dingtalk-message 66 | tags: 67 | - latest 68 | - 1.0.0 69 | 70 | trigger: 71 | event: 72 | - pull_request 73 | 74 | depends_on: 75 | - testing 76 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.y*ml] 2 | indent_size = 2 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "10:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/image.yaml: -------------------------------------------------------------------------------- 1 | name: Publish to DockerHub and Github Package 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | dockerhub: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | - name: Repo metadata 15 | id: repo 16 | uses: actions/github-script@v3 17 | with: 18 | script: | 19 | const repo = await github.repos.get(context.repo) 20 | return repo.data 21 | - name: Prepare 22 | id: prep 23 | run: | 24 | DOCKER_IMAGE=lddsb/drone-dingtalk-message 25 | GITHUB_IMAGE=ghcr.io/lddsb/drone-dingtalk-message 26 | VERSION=${GITHUB_REF#refs/tags/v} 27 | TAGS="${DOCKER_IMAGE}:${VERSION}" 28 | if [[ $VERSION =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then 29 | MINOR=${VERSION%.*} 30 | MAJOR=${MINOR%.*} 31 | TAGS="$TAGS,${DOCKER_IMAGE}:${MINOR},${DOCKER_IMAGE}:${MAJOR},${DOCKER_IMAGE}:latest" 32 | TAGS="$TAGS,${GITHUB_IMAGE}:${VERSION},${GITHUB_IMAGE}:${MINOR},${GITHUB_IMAGE}:${MAJOR},${GITHUB_IMAGE}:latest" 33 | fi 34 | echo ::set-output name=tags::${TAGS} 35 | - name: Setup Docker Buildx 36 | uses: docker/setup-buildx-action@v1 37 | - name: Login to DockerHub 38 | uses: docker/login-action@v1 39 | with: 40 | username: ${{ secrets.DOCKERHUB_USERNAME }} 41 | password: ${{ secrets.DOCKERHUB_TOKEN }} 42 | - name: Login to GitHub Container Register 43 | uses: docker/login-action@v1 44 | with: 45 | registry: ghcr.io 46 | username: ${{ github.repository_owner }} 47 | password: ${{ secrets.CR_PAT }} 48 | - name: Build and push 49 | id: docker_build 50 | uses: docker/build-push-action@v2 51 | with: 52 | context: . 53 | file: ./Dockerfile 54 | push: true 55 | tags: ${{ steps.prep.outputs.tags }} 56 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Publish release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | release: 10 | name: publish release, upload asset 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | - name: Set up Go 16 | uses: actions/setup-go@v5 17 | with: 18 | go-version: stable 19 | - name: Run GoReleaser 20 | uses: goreleaser/goreleaser-action@v5 21 | with: 22 | version: latest 23 | args: release --clean 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.CR_PAT }} 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dive.log 2 | drone-dingtalk-message 3 | .idea 4 | vendor 5 | coverage.txt 6 | coverage.out 7 | env.list -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | env: 2 | - GO111MODULE=on 3 | before: 4 | hooks: 5 | - go mod download 6 | builds: 7 | - env: 8 | - CGO_ENABLED=0 9 | goos: 10 | - linux 11 | - darwin 12 | - windows 13 | goarch: 14 | - 386 15 | - amd64 16 | - arm 17 | - arm64 18 | mod_timestamp: '{{ .CommitTimestamp }}' 19 | binary: dingtalk-message 20 | flags: 21 | - -trimpath 22 | ldflags: 23 | - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{ .CommitDate }} -X main.builtBy=goreleaser 24 | checksum: 25 | name_template: '{{ .ProjectName }}_checksums.txt' 26 | changelog: 27 | sort: asc 28 | filters: 29 | exclude: 30 | - '^docs:' 31 | - '^test:' 32 | - Merge pull request 33 | - Merge branch 34 | - go mod tidy 35 | archives: 36 | - name_template: >- 37 | {{- .ProjectName }}_ 38 | {{- title .Os }}_ 39 | {{- if eq .Arch "amd64" }}x86_64 40 | {{- else if eq .Arch "386" }}i386 41 | {{- else }}{{ .Arch }}{{ end }} 42 | {{- if .Arm }}v{{ .Arm }}{{ end -}} 43 | format_overrides: 44 | - goos: windows 45 | format: zip 46 | files: 47 | - README.md 48 | - LICENSE 49 | - tpls/* 50 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [Unreleased] 4 | 5 | ## [1.2.5] - 2020-12-16 6 | ### Added: 7 | * The TPL can use environment variables. 8 | * Debug mode. 9 | * Use GitHub Actions for automation. 10 | 11 | ## [1.2.4] - 2020-04-28 12 | ### Fixed: 13 | * kubernetes runner missing env, [details](https://docs.drone.io/runner/kubernetes/overview) 14 | 15 | ## [1.2.3] - 2020-01-20 16 | ### Fixed: 17 | * CDN url of default image. 18 | 19 | ## [1.2.2] - 2019-12-07 20 | ### Added: 21 | * Support DingTalk `sign secret`, please see the [README](README.md) for instructions. 22 | 23 | ## [1.2.1] - 2019-11-07 24 | ### Added: 25 | * Support customize message tips title by `tips_title` option. 26 | 27 | ## [1.2.0] - 2019-09-24 28 | ### Added: 29 | * Support custom TPL. 30 | 31 | ## [1.1.4] - 2020-04-28 32 | ### Fixed: 33 | * kubernetes runner missing env, [details](https://docs.drone.io/runner/kubernetes/overview) 34 | 35 | ## [1.1.3] - 2020-01-20 36 | ### Fixed: 37 | * CDN url of default image. 38 | 39 | ## [1.1.2] - 2019-11-07 40 | ### Added: 41 | * Support customize message tips title by `tips_title` option. 42 | 43 | ## [1.1.1] - 2019-09-24 44 | ### Added: 45 | * Support full token url as `token`. 46 | 47 | ## [1.1.0] - 2019-03-09 48 | ### Added: 49 | * Package management tools migrate from dep to go mod, 1.0.x version stopped supporting new features. 50 | 51 | ## [1.0.2] - 2020-01-20 52 | ### Fixed: 53 | * CDN url of default image. 54 | 55 | ## [1.0.1] - 2019-03-09 56 | ### Added: 57 | * Auto publish image to DockerHub. 58 | 59 | [Unreleased]: https://github.com/lddsb/drone-dingtalk-message/compare/v1.2.5...HEAD 60 | [1.2.5]: https://github.com/lddsb/drone-dingtalk-message/compare/v1.2.4...v1.2.5 61 | [1.2.4]: https://github.com/lddsb/drone-dingtalk-message/compare/v1.2.3...v1.2.4 62 | [1.2.3]: https://github.com/lddsb/drone-dingtalk-message/compare/v1.2.2...v1.2.3 63 | [1.2.2]: https://github.com/lddsb/drone-dingtalk-message/compare/v1.2.1...v1.2.2 64 | [1.2.1]: https://github.com/lddsb/drone-dingtalk-message/compare/v1.2.0...v1.2.1 65 | [1.2.0]: https://github.com/lddsb/drone-dingtalk-message/compare/v1.1.0...v1.2.0 66 | [1.1.4]: https://github.com/lddsb/drone-dingtalk-message/compare/v1.1.3...v1.1.4 67 | [1.1.3]: https://github.com/lddsb/drone-dingtalk-message/compare/v1.1.2...v1.1.3 68 | [1.1.2]: https://github.com/lddsb/drone-dingtalk-message/compare/v1.1.1...v1.1.2 69 | [1.1.1]: https://github.com/lddsb/drone-dingtalk-message/compare/v1.1.0...v1.1.1 70 | [1.1.0]: https://github.com/lddsb/drone-dingtalk-message/compare/v1.0.0...v1.1.0 71 | [1.0.2]: https://github.com/lddsb/drone-dingtalk-message/compare/v1.0.1...v1.0.2 72 | [1.0.1]: https://github.com/lddsb/drone-dingtalk-message/compare/v1.0.0...v1.0.1 73 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang AS builder 2 | WORKDIR /app 3 | COPY . . 4 | RUN GO111MODULE=on CGO_ENABLED=0 GOOS=linux go build -a -o drone-dingtalk . 5 | 6 | FROM alpine:latest 7 | RUN apk update && apk add ca-certificates && rm -rf /var/cache/apk/* 8 | COPY --from=builder /app/drone-dingtalk /bin 9 | COPY --from=builder /app/tpls /app/drone/dingtalk/message/tpls 10 | 11 | ENTRYPOINT ["/bin/drone-dingtalk"] 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Dee Luo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GO ?= go 2 | PACKAGES ?= $(shell $(GO) list ./...) 3 | 4 | vet: 5 | $(GO) vet $(PACKAGES) 6 | 7 | test: 8 | @$(GO) test -v -cover -coverprofile coverage.txt $(PACKAGES) && echo "\n==>\033[32m Ok\033[m\n" || exit 1 9 | 10 | coverage: 11 | sed -i '/main.go/d' coverage.txt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Drone CI DingTalk Message Plugin 2 | [![GitHub Actions](https://github.com/lddsb/drone-dingtalk-message/workflows/Publish%20to%20DockerHub%20and%20Github%20Package/badge.svg)](https://github.com/lddsb/drone-dingtalk-message/actions?query=workflow%3A%22Publish+to+DockerHub+and+Github+Package%22) [![Go Report Card](https://goreportcard.com/badge/github.com/lddsb/drone-dingtalk-message)](https://goreportcard.com/report/github.com/lddsb/drone-dingtalk-message) [![codecov](https://codecov.io/gh/lddsb/drone-dingtalk-message/branch/master/graph/badge.svg)](https://codecov.io/gh/lddsb/drone-dingtalk-message) [![Dependabot](https://api.dependabot.com/badges/status?host=github&repo=lddsb/drone-dingtalk-message&identifier=159822771)](https://app.dependabot.com/accounts/lddsb/repos/159822771) [![LICENSE: MIT](https://img.shields.io/github/license/lddsb/drone-dingtalk-message.svg?style=flat-square)](LICENSE) 3 | 4 | [中文说明](README_ZH.md) 5 | 6 | 7 | 8 | - [Drone CI Plugin Config](#drone-ci-plugin-config) 9 | - [Plugin Parameter Reference](#plugin-parameter-reference) 10 | - [TPL](#tpl) 11 | - [Screen Shot](#screen-shot) 12 | - [Development](#development) 13 | - [Todo](#todo) 14 | - [Kubernetes Users](#kubernetes-users) 15 | 16 | 17 | 18 | ### Drone CI Plugin Config 19 | `0.8.x` 20 | ```yaml 21 | pipeline: 22 | #... 23 | notification: 24 | image: lddsb/drone-dingtalk-message 25 | token: your-group-bot-token 26 | type: markdown 27 | ``` 28 | 29 | `1.x` 30 | ```yaml 31 | steps: 32 | #... 33 | - name: notification 34 | image: lddsb/drone-dingtalk-message 35 | settings: 36 | token: your-groupbot-token 37 | type: markdown 38 | secret: your-secret-for-generate-sign 39 | debug: true 40 | ``` 41 | 42 | `Use the "exec" type` 43 | ```yaml 44 | kind: pipeline 45 | type: exec 46 | 47 | steps: 48 | ... 49 | 50 | - name: notification 51 | environment: # Using environment to pass parameters 52 | PLUGIN_TOKEN: 53 | from_secret: dingtalk_token 54 | PLUGIN_TYPE: markdown 55 | PLUGIN_DEBUG: false 56 | PLUGIN_TPL: /data/drone/dingtalk/tpls/markdown.tpl # The actual location (absolute path) of the tpl. 57 | commands: 58 | - /data/drone/dingtalk/dingtalk-message # Location of the "dingtalk-message" file (absolute path) 59 | ``` 60 | 61 | ### Plugin Parameter Reference 62 | `token`(required) 63 | 64 | String. Access token for group bot. (you can get the access token when you add a bot in a group) 65 | 66 | `type`(required) 67 | 68 | String. Message type, plan support text, markdown, link and action card, but due to time issue, it's only support `markdown` and `text` now, and you can get the best experience by use markdown. 69 | 70 | `secret` 71 | 72 | String. Secret for generate sign. 73 | 74 | `tpl` 75 | 76 | String. Your custom `tpl`, it can be a local path, or a remote http link. 77 | 78 | `debug` 79 | Boolean. Debug mode. 80 | 81 | `tips_title` 82 | 83 | String. You can customize the title for the message tips, just work when message type is markdown. 84 | 85 | `success_color` 86 | 87 | String. You can customize the color for the `build success` message by this option, you should input a hex color, example: `008000`. 88 | 89 | `failure_color` 90 | 91 | String. You can customize the color for the `build success` message by this option, you should input a hex color, example: `FF0000`. 92 | 93 | `success_pic` 94 | 95 | String. You can customize the picture for the `build success` message by this option. 96 | 97 | `failure_pic` 98 | 99 | String. You can customize the picture for the `build failure` message by this option. 100 | 101 | `tpl_commit_branch_name` 102 | 103 | String. You can customize the [TPL_COMMIT_BRANCH] by this configuration item. 104 | 105 | `tpl_repo_short_name` 106 | 107 | String. You can customize the [TPL_REPO_SHORT_NAME] by this configuration item. 108 | 109 | `tpl_repo_full_name` 110 | 111 | String. You can customize the [TPL_REPO_FULL_NAME] by this configuration item. 112 | 113 | `tpl_build_status_success` 114 | 115 | String. You can customize the [TPL_BUILD_STATUS] (when status=`success`) by this configuration item. 116 | 117 | `tpl_build_status_failure` 118 | 119 | String. You can customize the [TPL_BUILD_STATUS] (when status=`failure`) by this configuration item. 120 | 121 | `msg_at_mobiles` 122 | 123 | String. You want at's phone number in the group, if you need at multi phone numbers, you can use `,` to separate. (if you use markdown type, you need define the at content in your tpl file) 124 | 125 | ### TPL 126 | > `tpl` won't work with message type `link` !!! 127 | 128 | That's a good news, we support `tpl` now.This is an example for `markdown` message: 129 | 130 | # [TPL_REPO_FULL_NAME] build [TPL_BUILD_STATUS], takes [TPL_BUILD_CONSUMING]s 131 | @mobile1 @mobile2 132 | [TPL_COMMIT_MSG] 133 | 134 | [TPL_COMMIT_SHA]([TPL_COMMIT_LINK]) 135 | 136 | [[TPL_AUTHOR_NAME]([TPL_AUTHOR_EMAIL])](mailto:[TPL_AUTHOR_EMAIL]) 137 | 138 | [Click To The Build Detail Page [TPL_STATUS_EMOTICON)]]([TPL_BUILD_LINK]) 139 | You can write your own `tpl` what you want. The syntax of `tpl` is very simple, you can fill `tpl` with preset variables. It's a list of currently supported preset variables: 140 | 141 | | Variable | Value | 142 | | :-------------------: | :-------------------------------------------------: | 143 | | [TPL_REPO_SHORT_NAME] | current repo name(bare name) | 144 | | [TPL_REPO_FULL_NAME] | the full name(with group name) of current repo | 145 | | [TPL_REPO_GROUP_NAME] | the group name of current repo | 146 | | [TPL_REPO_OWNER_NAME] | the owner name of current repo | 147 | | [TPL_REPO_REMOTE_URL] | the remote url of current repo | 148 | | [TPL_BUILD_STATUS] | current build status(e.g., success, failure) | 149 | | [TPL_BUILD_LINK] | current build link | 150 | | [TPL_BUILD_EVENT] | current build event(e.g., push, pull request, etc.) | 151 | | [TPL_BUILD_CONSUMING] | current build consuming, second | 152 | | [TPL_COMMIT_SHA] | current commit sha | 153 | | [TPL_COMMIT_REF] | current commit ref(e.g., refs/heads/master, etc.) | 154 | | [TPL_COMMIT_LINK] | current commit remote url link | 155 | | [TPL_COMMIT_BRANCH] | current branch name(e.g., dev, etc) | 156 | | [TPL_COMMIT_MSG] | current commit message | 157 | | [TPL_AUTHOR_NAME] | current commit author name | 158 | | [TPL_AUTHOR_EMAIL] | current commit author email | 159 | | [TPL_AUTHOR_USERNAME] | current commit author username | 160 | | [TPL_AUTHOR_AVATAR] | current commit author avatar | 161 | | [TPL_STATUS_PIC] | custom pic for build status | 162 | | [TPL_STATUS_COLOR] | custom color for build status | 163 | | [TPL_STATUS_EMOTICON] | custom emoticon for build status | 164 | 165 | 166 | 167 | ### Screen Shot 168 | - Send Success 169 | 170 | ![send-success](https://i.imgur.com/cECppkW.jpg) 171 | 172 | - Missing Access Token 173 | 174 | ![missing-access-token](https://i.imgur.com/Su7iiyw.jpg) 175 | 176 | - Missing Message Type Or Not Support Message Type 177 | 178 | ![message-type-error](https://i.imgur.com/qtJ4DsA.jpg) 179 | 180 | - Markdown DingTalk Message(default) 181 | 182 | ![markdown-message-default](https://i.imgur.com/Bl7cT1y.jpg) 183 | 184 | - Markdown DingTalk Message(color and sha link) 185 | 186 | ![markdown-massage-customize](https://i.imgur.com/pzdFzIw.jpg) 187 | 188 | - Markdown DingTalk Message(color, pic and sha link) 189 | 190 | ![markdown-massage-customize](https://i.imgur.com/xFrCTZp.jpg) 191 | 192 | 193 | ### Development 194 | We use `go mod` to manage dependencies, so it's easy to build. 195 | 196 | - get this repo 197 | ```shell 198 | $ git clone https://github.com/lddsb/drone-dingtalk-message.git /path/to/you/want 199 | ``` 200 | - build 201 | ```shell 202 | $ cd /path/to/you/want && GO111MODULE=on go build . 203 | ``` 204 | - run 205 | ```shell 206 | $ ./drone-dingtalk-message -h 207 | ``` 208 | 209 | ### TODO 210 | It's sad, just support `text`, `markdown` and `link` type now. 211 | - implement all message type 212 | - i18N 213 | - batch send 214 | - retry(e.g., network error, etc.) 215 | 216 | ### Kubernetes Users 217 | Attention kubernetes users, [CHANGELOG](CHANGELOG.md#124---2020-04-28).It's the available versions: 218 | 219 | - `1.1`(always latest for `1.1.x`) 220 | - `>=1.1.4` 221 | - `1.2`(always latest for `1.2.x`) 222 | - `>=1.2.4` 223 | - latest(always latest) 224 | -------------------------------------------------------------------------------- /README_ZH.md: -------------------------------------------------------------------------------- 1 | # Drone CI的钉钉群组机器人通知插件 2 | [![GitHub Actions](https://github.com/lddsb/drone-dingtalk-message/workflows/Publish%20to%20DockerHub%20and%20Github%20Package/badge.svg)](https://github.com/lddsb/drone-dingtalk-message/actions?query=workflow%3A%22Publish+to+DockerHub+and+Github+Package%22) [![Go Report Card](https://goreportcard.com/badge/github.com/lddsb/drone-dingtalk-message)](https://goreportcard.com/report/github.com/lddsb/drone-dingtalk-message) [![codecov](https://codecov.io/gh/lddsb/drone-dingtalk-message/branch/master/graph/badge.svg)](https://codecov.io/gh/lddsb/drone-dingtalk-message) [![Dependabot](https://api.dependabot.com/badges/status?host=github&repo=lddsb/drone-dingtalk-message&identifier=159822771)](https://app.dependabot.com/accounts/lddsb/repos/159822771) [![LICENSE: MIT](https://img.shields.io/github/license/lddsb/drone-dingtalk-message.svg?style=flat-square)](LICENSE) 3 | 4 | 5 | 6 | 7 | - [怎么使用本插件](#%E6%80%8E%E4%B9%88%E4%BD%BF%E7%94%A8%E6%9C%AC%E6%8F%92%E4%BB%B6) 8 | - [插件参数](#%E6%8F%92%E4%BB%B6%E5%8F%82%E6%95%B0) 9 | - [模版](#%E6%A8%A1%E7%89%88) 10 | - [截图展示](#%E6%88%AA%E5%9B%BE%E5%B1%95%E7%A4%BA) 11 | - [贡献代码](#%E8%B4%A1%E7%8C%AE%E4%BB%A3%E7%A0%81) 12 | - [未来计划](#%E6%9C%AA%E6%9D%A5%E8%AE%A1%E5%88%92) 13 | - [Kubernetes 用户请注意](#kubernetes-%E7%94%A8%E6%88%B7%E8%AF%B7%E6%B3%A8%E6%84%8F) 14 | 15 | 16 | 17 | ### 怎么使用本插件 18 | 添加一个`step`到你的`.drone.yml`中,下面是例子: 19 | 20 | `0.8.x` 21 | ```yaml 22 | pipeline: 23 | ... 24 | notification: 25 | image: lddsb/drone-dingtalk-message 26 | token: your-group-bot-token 27 | type: markdown 28 | ``` 29 | 30 | `1.x` 31 | ```yaml 32 | steps: 33 | ... 34 | - name: notification 35 | image: lddsb/drone-dingtalk-message 36 | settings: 37 | token: your-groupbot-token 38 | type: markdown 39 | secret: your-secret-for-generate-sign 40 | debug: true 41 | ``` 42 | 43 | `命令行版本` 44 | ```yaml 45 | kind: pipeline 46 | type: exec 47 | 48 | steps: 49 | ... 50 | 51 | - name: 通知 52 | environment: # 使用 environment 传递参数 53 | PLUGIN_TOKEN: 54 | from_secret: dingtalk_token 55 | PLUGIN_TYPE: markdown 56 | PLUGIN_DEBUG: false 57 | PLUGIN_TPL: /data/drone/dingtalk/tpls/markdown.tpl # tpl 的实际位置(绝对路径) 58 | commands: 59 | - /data/drone/dingtalk/dingtalk-message # dingtalk-message 文件的位置(绝对路径) 60 | ``` 61 | 62 | ### 插件参数 63 | `token`(必须) 64 | 65 | 你可以通过加入和创建一个群组来添加钉钉自定义机器人,添加自定义机器人完成后即可获得所需要的`access token`。 66 | 67 | `type`(必须) 68 | 69 | 消息类型,因个人能力有限,目前仅支持`markdown`和`text`,其中,使用`markdown`可以获得最好的体验。 70 | 71 | `secret` 72 | 73 | 如果你设置了`加签`,可以把你的`加签`密钥填入此项完成`加签`操作。 74 | 75 | `tpl` 76 | 77 | 你可以通过该字段来自定义你的消息模版。该字段可以是一个本地路径也可以是一个远程的URL。 78 | 79 | `debug` 80 | 81 | 通过该值可以打开`debug`模式,打印所有环境变量。 82 | 83 | `tips_title` 84 | 85 | 你可以通过该字段自定义钉钉机器人的消息通知提醒标题。(注意,不是消息内容的标题,是收到钉钉机器人发的消息后,会有一个外显的标题) 86 | 87 | `success_color` 88 | 89 | 你可以通过该字段自定义打包成功的颜色。比如:`008000`。 90 | 91 | `failure_color` 92 | 93 | 你可以通过该字段自定义打包失败的颜色。比如:`FF0000`。 94 | 95 | `success_pic` 96 | 97 | 你可以通过该字段自定义打包成功的图片。 98 | 99 | `failure_pic` 100 | 101 | 字符串,你可以通过该字段自定义打包失败的图片。 102 | 103 | `tpl_commit_branch_name` 104 | 105 | 你可以通过该字段自定义分支的名称,可以在模版中通过[TPL_COMMIT_BRANCH]来使用该值。 106 | 107 | `tpl_repo_short_name` 108 | 109 | 你可以通过该字段自定义仓库的名字,可以在模版中通过[TPL_REPO_SHORT_NAME]来使用该值。 110 | 111 | `tpl_repo_full_name` 112 | 113 | 你可以通过该字段自定义仓库的全名(包含组织名称),可以在模版中通过[TPL_REPO_FULL_NAME]来使用该值。 114 | 115 | `tpl_build_status_success` 116 | 117 | 你可以通过该字段自定义运行成功状态的值,可以在模版中通过[TPL_BUILD_STATUS]来使用该值。(仅当前方`step`运行结果为成功时该值会生效) 118 | 119 | `tpl_build_status_failure` 120 | 121 | 你可以通过该字段自定义运行失败状态的值,可以在模版中通过[TPL_BUILD_STATUS]来使用该值。(仅当前方`step`运行结果为失败时该值会生效) 122 | 123 | `msg_at_mobiles` 124 | 125 | 你需要@的群成员的手机号,多个时用英文逗号(`,`)分隔。如过你使用的是 `markdown` 类型的消息,则需要在 `tpl` 文件中加入 `@手机号` 的内容。 126 | 127 | ### 模版 128 | > `tpl` 对 `link` 类型的消息并不支持 !!! 129 | 130 | 感天动地,我们终于支持自定义模版了!下面是一个`markdown`的自定义模版例子: 131 | 132 | # [TPL_REPO_FULL_NAME] build [TPL_BUILD_STATUS], takes [TPL_BUILD_CONSUMING]s 133 | @mobile1 @mobile2 134 | [TPL_COMMIT_MSG] 135 | 136 | [TPL_COMMIT_SHA]([TPL_COMMIT_LINK]) 137 | 138 | [[TPL_AUTHOR_NAME]([TPL_AUTHOR_EMAIL])](mailto:[TPL_AUTHOR_EMAIL]) 139 | 140 | [Click To The Build Detail Page [TPL_STATUS_EMOTICON)]]([TPL_BUILD_LINK]) 141 | 142 | `mobile1` 和 `mobile2` 应该为钉钉对应的手机号码,可以放在自己想要放的位置。 143 | 144 | 你可以写自己喜欢的模版,终于不用再对默认模版发愁啦!并且模版的语法非常简单!比较可惜的是目前支持的变量还比较少,下面是当前支持的变量的列表: 145 | 146 | | Variable | Value | 147 | | :-------------------: | :-------------------------------------------------: | 148 | | [TPL_REPO_SHORT_NAME] | 当前仓库的名称,比如本仓库 `drone-dingtalk-message` | 149 | | [TPL_REPO_FULL_NAME] | 当前仓库的名称,比如本仓库 `lddsb/drone-dingtalk-message` | 150 | | [TPL_REPO_GROUP_NAME] | 当前仓库的组织名称,比如本仓库 `lddsb` | 151 | | [TPL_REPO_OWNER_NAME] | 当前仓库拥有者的名称 | 152 | | [TPL_REPO_REMOTE_URL] | 当前仓库的远程地址 | 153 | | [TPL_BUILD_STATUS] | 当前编译的状态(比如, success, failure) | 154 | | [TPL_BUILD_LINK] | 当前编译的链接 | 155 | | [TPL_BUILD_EVENT] | 触发当前编译的动作(比如, push, pull request等) | 156 | | [TPL_BUILD_CONSUMING] | 当前编译耗时,单位秒 | 157 | | [TPL_COMMIT_SHA] | 当前提交的sha | 158 | | [TPL_COMMIT_REF] | 当前提交的ref(比如, refs/heads/master等) | 159 | | [TPL_COMMIT_LINK] | 当前提交的远程地址 | 160 | | [TPL_COMMIT_BRANCH] | 当前分之名称(比如, dev, master等) | 161 | | [TPL_COMMIT_MSG] | 当前提交的信息 | 162 | | [TPL_AUTHOR_NAME] | 当前提交作者名称 | 163 | | [TPL_AUTHOR_EMAIL] | 当前提交作者邮箱地址 | 164 | | [TPL_AUTHOR_USERNAME] | 当前提交作者的用户名 | 165 | | [TPL_AUTHOR_AVATAR] | 当前提交作者的头像 | 166 | | [TPL_STATUS_PIC] | 根据编译状态显示不同的图片 | 167 | | [TPL_STATUS_COLOR] | 根据编译状态显示不同的颜色 | 168 | | [TPL_STATUS_EMOTICON] | 根据编译状态显示不同的表情,比如 `:)` `:(` | 169 | 170 | 171 | 172 | ### 截图展示 173 | - 发送成功(Drone Web) 174 | 175 | ![send-success](https://i.imgur.com/cECppkW.jpg) 176 | 177 | - 忘记填写Access Token(Drone Web) 178 | 179 | ![missing-access-token](https://i.imgur.com/Su7iiyw.jpg) 180 | 181 | - 忘记填写消息类型或者不支持的消息类型 182 | 183 | ![message-type-error](https://i.imgur.com/qtJ4DsA.jpg) 184 | 185 | - 默认的`markdown`消息 186 | 187 | ![markdown-message-default](https://i.imgur.com/Bl7cT1y.jpg) 188 | 189 | - 带颜色和链接的`markdown`消息 190 | 191 | ![markdown-massage-customize](https://i.imgur.com/pzdFzIw.jpg) 192 | 193 | - 带颜色、链接和图片的`markdown`消息 194 | 195 | ![markdown-massage-customize](https://i.imgur.com/xFrCTZp.jpg) 196 | 197 | 198 | ### 贡献代码 199 | 本项目使用了`go mod`来管理依赖,因此要编译本项目相当简单。 200 | 201 | - 先把项目代码拷贝到本地 202 | ```shell 203 | $ git clone https://github.com/lddsb/drone-dingtalk-message.git /path/to/you/want 204 | ``` 205 | - 然后直接执行编译即可 206 | ```shell 207 | $ cd /path/to/you/want && GO111MODULE=on go build . 208 | ``` 209 | - 跑个`help` 210 | ```shell 211 | $ ./drone-dingtalk-message -h 212 | ``` 213 | 214 | ### 未来计划 215 | 目前仅支持 `text`, `markdown` 以及 `link` 类型的消息,建议使用`markdown`类型。 216 | - 实现更多的消息类型 217 | - i18N国际化直接翻译环境变量 218 | - 批量发送给多个群机器人 219 | - 失败重试机制 220 | 221 | ### Kubernetes 用户请注意 222 | 因为`Drone CI` [官方缺陷](https://docs.drone.io/runner/kubernetes/overview) ,所以较早版本将无法正常获取到需要用到的变量,会导致部分功能异常。为了能正常使用,所以请使用以下版本: 223 | - `1.1`(总会是`1.1.x`的最新版本) 224 | - `>=1.1.4` 225 | - `1.2`(总会是`1.2.x`的最新版本) 226 | - `>=1.2.4` 227 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lddsb/drone-dingtalk-message 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/joho/godotenv v1.5.1 7 | github.com/lddsb/dingtalk-webhook v0.0.5 8 | github.com/urfave/cli v1.22.15 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 2 | github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= 3 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 8 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 9 | github.com/lddsb/dingtalk-webhook v0.0.5 h1:EOSXvcpN4IC7fXSAheI3OLJwwOGEPlGDV7xje1fQuJo= 10 | github.com/lddsb/dingtalk-webhook v0.0.5/go.mod h1:dwNU75Sog87wJXAFcY5mDFM7eW4hIdX7bNemrN92pH0= 11 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 12 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 13 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 14 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 15 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 16 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 17 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 18 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 19 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 20 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 21 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 22 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 23 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 24 | github.com/urfave/cli v1.22.15 h1:nuqt+pdC/KqswQKhETJjo7pvn/k4xMUxgW6liI7XpnM= 25 | github.com/urfave/cli v1.22.15/go.mod h1:wSan1hmo5zeyLGBjRJbzRTNk8gwoYa2B9n4q9dmRIc0= 26 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 27 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 28 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 29 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 30 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 31 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 32 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "time" 8 | 9 | "github.com/joho/godotenv" 10 | "github.com/urfave/cli" 11 | ) 12 | 13 | // Version of cli 14 | var Version = "0.2.1219" 15 | 16 | func main() { 17 | app := cli.NewApp() 18 | app.Name = "Drone DingTalk Message Plugin" 19 | app.Usage = "Sending message to DingTalk group by robot using WebHook" 20 | year := time.Now().Year() 21 | app.Copyright = fmt.Sprintf("© 2018-%d Dee Luo", year) 22 | app.Authors = []cli.Author{ 23 | { 24 | Name: "Dee Luo", 25 | Email: "luodi0128@gmail.com", 26 | }, 27 | } 28 | app.Action = run 29 | app.Version = Version 30 | app.Flags = []cli.Flag{ 31 | cli.BoolFlag{ 32 | Name: "config.debug,debug", 33 | Usage: "debug mode", 34 | EnvVar: "PLUGIN_DEBUG", 35 | }, 36 | cli.StringFlag{ 37 | Name: "config.tips.title", 38 | Usage: "customize the tips title", 39 | EnvVar: "PLUGIN_TIPS_TITLE", 40 | }, 41 | cli.StringFlag{ 42 | Name: "config.token,access_token,token", 43 | Usage: "DingTalk webhook access token", 44 | EnvVar: "PLUGIN_ACCESS_TOKEN,PLUGIN_TOKEN", 45 | }, 46 | cli.StringFlag{ 47 | Name: "config.secret,secret", 48 | Usage: "DingTalk WebHook secret for generate sign", 49 | EnvVar: "PLUGIN_SECRET", 50 | }, 51 | cli.StringFlag{ 52 | Name: "config.message.type,message_type,type", 53 | Usage: "DingTalk message type, like text, markdown, action card, link and feed card...", 54 | EnvVar: "PLUGIN_MSG_TYPE,PLUGIN_TYPE,PLUGIN_MESSAGE_TYPE", 55 | }, 56 | cli.StringFlag{ 57 | Name: "config.message.at.all,at.all", 58 | Usage: "at all in a message(only text and markdown type message can at)", 59 | EnvVar: "PLUGIN_MSG_AT_ALL", 60 | }, 61 | cli.StringFlag{ 62 | Name: "config.message.at.mobiles,mobiles", 63 | Usage: "at someone in a DingTalk group need this guy bind's mobile", 64 | EnvVar: "PLUGIN_MSG_AT_MOBILES", 65 | }, 66 | cli.StringFlag{ 67 | Name: "commit.author.username", 68 | Usage: "providers the author username for the current commit", 69 | EnvVar: "DRONE_COMMIT_AUTHOR", 70 | }, 71 | cli.StringFlag{ 72 | Name: "commit.author.avatar", 73 | Usage: "providers the author avatar url for the current commit", 74 | EnvVar: "DRONE_COMMIT_AUTHOR_AVATAR", 75 | }, 76 | cli.StringFlag{ 77 | Name: "commit.author.email", 78 | Usage: "providers the author email for the current commit", 79 | EnvVar: "DRONE_COMMIT_AUTHOR_EMAIL", 80 | }, 81 | cli.StringFlag{ 82 | Name: "commit.author.name", 83 | Usage: "providers the author name for the current commit", 84 | EnvVar: "DRONE_COMMIT_AUTHOR", 85 | }, 86 | cli.StringFlag{ 87 | Name: "commit.branch", 88 | Usage: "providers the branch for the current build", 89 | EnvVar: "DRONE_COMMIT_BRANCH", 90 | Value: "master", 91 | }, 92 | cli.StringFlag{ 93 | Name: "commit.link", 94 | Usage: "providers the http link to the current commit in the remote source code management system(e.g.GitHub)", 95 | EnvVar: "DRONE_COMMIT_LINK", 96 | }, 97 | cli.StringFlag{ 98 | Name: "commit.message", 99 | Usage: "providers the commit message for the current build", 100 | EnvVar: "DRONE_COMMIT_MESSAGE", 101 | }, 102 | cli.StringFlag{ 103 | Name: "commit.sha", 104 | Usage: "providers the commit sha for the current build", 105 | EnvVar: "DRONE_COMMIT_SHA", 106 | }, 107 | cli.StringFlag{ 108 | Name: "commit.ref", 109 | Usage: "provider the commit ref for the current build", 110 | EnvVar: "DRONE_COMMIT_REF", 111 | }, 112 | cli.StringFlag{ 113 | Name: "repo.full.name", 114 | Usage: "providers the full name of the repository", 115 | EnvVar: "DRONE_REPO", 116 | }, 117 | cli.StringFlag{ 118 | Name: "repo.name", 119 | Usage: "provider the name of the repository", 120 | EnvVar: "DRONE_REPO_NAME", 121 | }, 122 | cli.StringFlag{ 123 | Name: "repo.group", 124 | Usage: "provider the group of the repository", 125 | EnvVar: "DRONE_REPO_NAMESPACE", 126 | }, 127 | cli.StringFlag{ 128 | Name: "repo.remote.url", 129 | Usage: "provider the remote url of the repository", 130 | EnvVar: "DRONE_REMOTE_URL", 131 | }, 132 | cli.StringFlag{ 133 | Name: "repo.owner", 134 | Usage: "provider the owner of the repository", 135 | EnvVar: "DRONE_REPO_OWNER", 136 | }, 137 | cli.Uint64Flag{ 138 | Name: "stage.started", 139 | Usage: "stage started ", 140 | EnvVar: "DRONE_STAGE_STARTED", 141 | }, 142 | cli.Uint64Flag{ 143 | Name: "stage.finished", 144 | Usage: "stage finished", 145 | EnvVar: "DRONE_STAGE_FINISHED", 146 | }, 147 | cli.StringFlag{ 148 | Name: "build.status", 149 | Usage: "build status", 150 | Value: "success", 151 | EnvVar: "DRONE_BUILD_STATUS", 152 | }, 153 | cli.StringFlag{ 154 | Name: "build.link", 155 | Usage: "build link", 156 | EnvVar: "DRONE_BUILD_LINK", 157 | }, 158 | cli.StringFlag{ 159 | Name: "build.event", 160 | Usage: "build event", 161 | EnvVar: "DRONE_BUILD_EVENT", 162 | }, 163 | cli.Uint64Flag{ 164 | Name: "build.started", 165 | Usage: "build started", 166 | EnvVar: "DRONE_BUILD_STARTED", 167 | }, 168 | cli.Uint64Flag{ 169 | Name: "build.finished", 170 | Usage: "build finished", 171 | EnvVar: "DRONE_BUILD_FINISHED", 172 | }, 173 | cli.StringFlag{ 174 | Name: "tpl.build.status.success", 175 | Usage: "tpl.build status for replace success", 176 | EnvVar: "TPL_BUILD_STATUS_SUCCESS, PLUGIN_TPL_BUILD_STATUS_SUCCESS", 177 | }, 178 | cli.StringFlag{ 179 | Name: "tpl.build.status.failure", 180 | Usage: "tpl.build status for replace failure", 181 | EnvVar: "TPL_BUILD_STATUS_FAILURE, PLUGIN_TPL_BUILD_STATUS_FAILURE", 182 | }, 183 | cli.StringFlag{ 184 | Name: "custom.pic.url.success", 185 | Usage: "custom success picture url", 186 | EnvVar: "SUCCESS_PICTURE_URL,PLUGIN_SUCCESS_PIC", 187 | }, 188 | cli.StringFlag{ 189 | Name: "custom.pic.url.failure", 190 | Usage: "custom failure picture url", 191 | EnvVar: "FAILURE_PICTURE_URL,PLUGIN_FAILURE_PIC", 192 | }, 193 | cli.StringFlag{ 194 | Name: "custom.color.success", 195 | Usage: "custom success color for title in markdown", 196 | EnvVar: "SUCCESS_COLOR,PLUGIN_SUCCESS_COLOR", 197 | }, 198 | cli.StringFlag{ 199 | Name: "custom.color.failure", 200 | Usage: "custom failure color for title in markdown", 201 | EnvVar: "FAILURE_COLOR,PLUGIN_FAILURE_COLOR", 202 | }, 203 | cli.StringFlag{ 204 | Name: "custom.tpl,tpl", 205 | Usage: "custom tpl", 206 | EnvVar: "PLUGIN_TPL,PLUGIN_CUSTOM_TPL", 207 | }, 208 | cli.StringFlag{ 209 | Name: "tpl.repo.full.name", 210 | Usage: "tpl custom repo full name", 211 | EnvVar: "PLUGIN_TPL_REPO_FULL_NAME,TPL_REPO_FULL_NAME", 212 | }, 213 | cli.StringFlag{ 214 | Name: "tpl.repo.short.name", 215 | Usage: "tpl custom repo short name", 216 | EnvVar: "PLUGIN_TPL_REPO_SHORT_NAME,TPL_REPO_SHORT_NAME", 217 | }, 218 | cli.StringFlag{ 219 | Name: "tpl.commit.branch.name", 220 | Usage: "tpl custom commit branch name", 221 | EnvVar: "PLUGIN_TPL_COMMIT_BRANCH_NAME,TPL_COMMIT_BRANCH_NAME", 222 | }, 223 | cli.StringFlag{ 224 | Name: "custom.started,started", 225 | Usage: "started custom env name, eg., BUILD_STARTED", 226 | EnvVar: "PLUGIN_CUSTOM_STARTED", 227 | }, 228 | cli.StringFlag{ 229 | Name: "custom.finished,finished", 230 | Usage: "finished custom env name, eg., BUILD_FINISHED", 231 | EnvVar: "PLUGIN_CUSTOM_FINISHED", 232 | }, 233 | } 234 | 235 | // kubernetes runner patch 236 | if _, err := os.Stat("/run/drone/env"); err == nil { 237 | godotenv.Overload("/run/drone/env") 238 | } 239 | 240 | if err := app.Run(os.Args); nil != err { 241 | log.Println(err) 242 | } 243 | } 244 | 245 | // run with args 246 | func run(c *cli.Context) { 247 | plugin := Plugin{ 248 | Drone: Drone{ 249 | // repo info 250 | Repo: Repo{ 251 | ShortName: c.String("repo.name"), 252 | GroupName: c.String("repo.group"), 253 | OwnerName: c.String("repo.owner"), 254 | RemoteURL: c.String("repo.remote.url"), 255 | FullName: c.String("repo.full.name"), 256 | }, 257 | // build info 258 | Build: Build{ 259 | Status: c.String("build.status"), 260 | Link: c.String("build.link"), 261 | Event: c.String("build.event"), 262 | StartAt: c.Uint64("build.started"), 263 | FinishedAt: c.Uint64("build.finished"), 264 | }, 265 | Commit: Commit{ 266 | Sha: c.String("commit.sha"), 267 | Branch: c.String("commit.branch"), 268 | Message: c.String("commit.message"), 269 | Link: c.String("commit.link"), 270 | Author: CommitAuthor{ 271 | Avatar: c.String("commit.author.avatar"), 272 | Email: c.String("commit.author.email"), 273 | Name: c.String("commit.author.name"), 274 | Username: c.String("commit.author.username"), 275 | }, 276 | }, 277 | Stage: Stage{ 278 | StartedAt: c.Uint64("stage.started"), 279 | FinishedAt: c.Uint64("stage.finished"), 280 | }, 281 | }, 282 | // custom config 283 | Config: Config{ 284 | AccessToken: c.String("config.token"), 285 | Secret: c.String("config.secret"), 286 | IsAtALL: c.Bool("config.message.at.all"), 287 | MsgType: c.String("config.message.type"), 288 | Mobiles: c.String("config.message.at.mobiles"), 289 | Debug: c.Bool("config.debug"), 290 | TipsTitle: c.String("config.tips.title"), 291 | }, 292 | Custom: Custom{ 293 | Pic: Pic{ 294 | SuccessPicURL: c.String("custom.pic.url.success"), 295 | FailurePicURL: c.String("custom.pic.url.failure"), 296 | }, 297 | Color: Color{ 298 | SuccessColor: c.String("custom.color.success"), 299 | FailureColor: c.String("custom.color.failure"), 300 | }, 301 | Tpl: c.String("custom.tpl"), 302 | Consuming: Consuming{ 303 | StartedEnv: c.String("custom.started"), 304 | FinishedEnv: c.String("custom.finished"), 305 | }, 306 | }, 307 | Tpl: Tpl{ 308 | Repo: TplRepo{ 309 | FullName: c.String("tpl.repo.full.name"), 310 | ShortName: c.String("tpl.repo.short.name"), 311 | }, 312 | Commit: TplCommit{ 313 | Branch: c.String("tpl.commit.branch.name"), 314 | }, 315 | Build: TplBuild{ 316 | Status: Status{ 317 | Success: c.String("tpl.build.status.success"), 318 | Failure: c.String("tpl.build.status.failure"), 319 | }, 320 | }, 321 | }, 322 | } 323 | 324 | if err := plugin.Exec(); nil != err { 325 | fmt.Println(err) 326 | os.Exit(1) 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /plugin.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "net/http" 9 | "net/url" 10 | "os" 11 | "regexp" 12 | "strconv" 13 | "strings" 14 | 15 | webhook "github.com/lddsb/dingtalk-webhook" 16 | ) 17 | 18 | type ( 19 | // Repo repo base info 20 | Repo struct { 21 | ShortName string // short name 22 | GroupName string // group name 23 | FullName string // repository full name 24 | OwnerName string // repo owner 25 | RemoteURL string // repo remote url 26 | } 27 | 28 | // Build info 29 | Build struct { 30 | Status string // providers the current build status 31 | Link string // providers the current build link 32 | Event string // trigger event 33 | StartAt uint64 // build start at ( unix timestamp ) 34 | FinishedAt uint64 // build finish at ( unix timestamp ) 35 | } 36 | 37 | // Commit info 38 | Commit struct { 39 | Branch string // providers the branch for the current commit 40 | Link string // providers the http link to the current commit in the remote source code management system(e.g.GitHub) 41 | Message string // providers the commit message for the current build 42 | Sha string // providers the commit sha for the current build 43 | Ref string // commit ref 44 | Author CommitAuthor 45 | } 46 | 47 | // Stage drone stage env 48 | Stage struct { 49 | StartedAt uint64 50 | FinishedAt uint64 51 | } 52 | 53 | // CommitAuthor commit author info 54 | CommitAuthor struct { 55 | Avatar string // providers the author avatar for the current commit 56 | Email string // providers the author email for the current commit 57 | Name string // providers the author name for the current commit 58 | Username string // the author username for the current commit 59 | } 60 | 61 | // Drone drone info 62 | Drone struct { 63 | Repo Repo 64 | Build Build 65 | Commit Commit 66 | Stage Stage 67 | } 68 | 69 | // Config plugin private config 70 | Config struct { 71 | Debug bool 72 | AccessToken string 73 | Secret string 74 | IsAtALL bool 75 | Mobiles string 76 | Username string 77 | MsgType string 78 | TipsTitle string 79 | } 80 | 81 | // MessageConfig DingTalk message struct 82 | MessageConfig struct { 83 | ActionCard ActionCard 84 | } 85 | 86 | // ActionCard action card message struct 87 | ActionCard struct { 88 | LinkUrls string 89 | LinkTitles string 90 | HideAvatar bool 91 | BtnOrientation bool 92 | } 93 | 94 | // Pic extra config for pic 95 | Pic struct { 96 | SuccessPicURL string 97 | FailurePicURL string 98 | } 99 | 100 | // Color extra config for color 101 | Color struct { 102 | SuccessColor string 103 | FailureColor string 104 | } 105 | 106 | // Plugin plugin all config 107 | Plugin struct { 108 | Tpl Tpl 109 | Drone Drone 110 | Config Config 111 | Custom Custom 112 | Message MessageConfig 113 | } 114 | 115 | // Custom user custom env 116 | Custom struct { 117 | Tpl string 118 | Color Color 119 | Pic Pic 120 | Consuming Consuming 121 | } 122 | 123 | // Tpl base 124 | Tpl struct { 125 | Repo TplRepo 126 | Commit TplCommit 127 | Build TplBuild 128 | } 129 | 130 | // TplRepo TPL repo 131 | TplRepo struct { 132 | FullName string 133 | ShortName string 134 | } 135 | 136 | // TplCommit TPL commit 137 | TplCommit struct { 138 | Branch string 139 | } 140 | 141 | // TplBuild TPL build 142 | TplBuild struct { 143 | Status Status 144 | } 145 | 146 | // Status status 147 | Status struct { 148 | Success string 149 | Failure string 150 | } 151 | 152 | // Consuming custom consuming env 153 | Consuming struct { 154 | StartedEnv string 155 | FinishedEnv string 156 | } 157 | ) 158 | 159 | // Exec execute WebHook 160 | func (p *Plugin) Exec() error { 161 | if p.Config.Debug { 162 | for _, e := range os.Environ() { 163 | log.Println(e) 164 | } 165 | } 166 | 167 | var err error 168 | if "" == p.Config.AccessToken { 169 | msg := "missing DingTalk access token" 170 | return errors.New(msg) 171 | } 172 | 173 | tpl, err := p.getMessage() 174 | if err != nil { 175 | return err 176 | } 177 | 178 | if p.Config.TipsTitle == "" { 179 | p.Config.TipsTitle = "you have a new message" 180 | } 181 | 182 | newWebHook := webhook.NewWebHook(p.Config.AccessToken) 183 | 184 | // add sign 185 | if "" != p.Config.Secret { 186 | newWebHook.Secret = p.Config.Secret 187 | } 188 | 189 | mobiles := strings.Split(p.Config.Mobiles, ",") 190 | switch strings.ToLower(p.Config.MsgType) { 191 | case "markdown": 192 | err = newWebHook.SendMarkdownMsg(p.Config.TipsTitle, tpl, p.Config.IsAtALL, mobiles...) 193 | case "text": 194 | err = newWebHook.SendTextMsg(tpl, p.Config.IsAtALL, mobiles...) 195 | case "link": 196 | err = newWebHook.SendLinkMsg(p.Drone.Build.Status, tpl, p.Drone.Commit.Author.Avatar, p.Drone.Build.Link) 197 | default: 198 | msg := "not support message type" 199 | err = errors.New(msg) 200 | } 201 | 202 | if err == nil { 203 | log.Println("send message success!") 204 | } 205 | 206 | return err 207 | } 208 | 209 | // fileExists check file is exists 210 | func fileExists(filePath string) bool { 211 | _, err := os.Stat(filePath) 212 | if err != nil { 213 | if os.IsExist(err) { 214 | return true 215 | } 216 | return false 217 | } 218 | return true 219 | } 220 | 221 | // getTpl get tpl from local file or remote file 222 | func (p *Plugin) getTpl() (tpl string, err error) { 223 | //var tpl string 224 | tplDir := "/app/drone/dingtalk/message/tpls" 225 | if "" == p.Custom.Tpl { 226 | p.Custom.Tpl = fmt.Sprintf("%s/%s.tpl", tplDir, strings.ToLower(p.Config.MsgType)) 227 | } 228 | 229 | u, err := url.Parse(p.Custom.Tpl) 230 | if err != nil { 231 | return "", err 232 | } 233 | 234 | if u.Scheme != "" { 235 | resp, err := http.Get(p.Custom.Tpl) 236 | if err != nil { 237 | return "", err 238 | } 239 | 240 | // check response 241 | if u.Path != resp.Request.URL.Path { 242 | return "", errors.New("cannot get tpl from url") 243 | } 244 | 245 | // defer close 246 | defer func() { 247 | _ = resp.Body.Close() 248 | }() 249 | 250 | body, err := ioutil.ReadAll(resp.Body) 251 | if err != nil { 252 | return "", err 253 | } 254 | 255 | tpl = string(body) 256 | } else { 257 | if !fileExists(p.Custom.Tpl) { 258 | // it must be a tpl stream 259 | return p.Custom.Tpl, nil 260 | } 261 | 262 | tplStr, err := ioutil.ReadFile(p.Custom.Tpl) 263 | if err != nil { 264 | return "", err 265 | } 266 | 267 | tpl = string(tplStr) 268 | } 269 | 270 | return tpl, nil 271 | } 272 | 273 | // fillTpl fill the tpl by valid keyword 274 | func (p *Plugin) fillTpl(tpl string) string { 275 | envs := p.getEnvs() 276 | // replace regex 277 | reg := regexp.MustCompile(`\[([^\[\]]*)]`) 278 | match := reg.FindAllStringSubmatch(tpl, -1) 279 | for _, m := range match { 280 | // from environment 281 | if envStr := os.Getenv(m[1]); envStr != "" { 282 | tpl = strings.ReplaceAll(tpl, m[0], envStr) 283 | } 284 | 285 | // check if the keyword is legal 286 | if _, ok := envs[m[1]]; ok { 287 | // replace keyword 288 | tpl = strings.ReplaceAll(tpl, m[0], envs[m[1]].(string)) 289 | } 290 | } 291 | 292 | return tpl 293 | } 294 | 295 | // getEnvs get available envs 296 | func (p *Plugin) getEnvs() map[string]interface{} { 297 | var envs map[string]interface{} 298 | envs = make(map[string]interface{}) 299 | envs["TPL_REPO_FULL_NAME"] = p.Drone.Repo.FullName 300 | if p.Tpl.Repo.FullName != "" { 301 | envs["TPL_REPO_FULL_NAME"] = p.Tpl.Repo.FullName 302 | } 303 | envs["TPL_REPO_SHORT_NAME"] = p.Drone.Repo.ShortName 304 | if p.Tpl.Repo.ShortName != "" { 305 | envs["TPL_REPO_SHORT_NAME"] = p.Tpl.Repo.ShortName 306 | } 307 | envs["TPL_REPO_GROUP_NAME"] = p.Drone.Repo.GroupName 308 | envs["TPL_REPO_OWNER_NAME"] = p.Drone.Repo.OwnerName 309 | envs["TPL_REPO_REMOTE_URL"] = p.Drone.Repo.RemoteURL 310 | 311 | envs["TPL_BUILD_STATUS"] = p.getStatus() 312 | envs["TPL_BUILD_LINK"] = p.Drone.Build.Link 313 | envs["TPL_BUILD_EVENT"] = p.Drone.Build.Event 314 | 315 | var consuming uint64 316 | // custom consuming env 317 | if p.Custom.Consuming.FinishedEnv != "" && p.Custom.Consuming.StartedEnv != "" { 318 | finishedAt, _ := strconv.ParseUint(os.Getenv(p.Custom.Consuming.FinishedEnv), 10, 64) 319 | startedAt, _ := strconv.ParseUint(os.Getenv(p.Custom.Consuming.StartedEnv), 10, 64) 320 | consuming = finishedAt - startedAt 321 | } else { 322 | consuming = p.Drone.Build.FinishedAt - p.Drone.Build.StartAt 323 | if consuming == 0 { 324 | consuming = p.Drone.Stage.FinishedAt - p.Drone.Stage.StartedAt 325 | } 326 | } 327 | envs["TPL_BUILD_CONSUMING"] = fmt.Sprintf("%v", consuming) 328 | 329 | envs["TPL_COMMIT_SHA"] = p.Drone.Commit.Sha 330 | envs["TPL_COMMIT_REF"] = p.Drone.Commit.Ref 331 | envs["TPL_COMMIT_LINK"] = p.Drone.Commit.Link 332 | envs["TPL_COMMIT_MSG"] = p.Drone.Commit.Message 333 | envs["TPL_COMMIT_BRANCH"] = p.Drone.Commit.Branch 334 | if p.Tpl.Commit.Branch != "" { 335 | envs["TPL_COMMIT_BRANCH"] = p.Tpl.Commit.Branch 336 | } 337 | 338 | envs["TPL_AUTHOR_NAME"] = p.Drone.Commit.Author.Name 339 | envs["TPL_AUTHOR_USERNAME"] = p.Drone.Commit.Author.Username 340 | envs["TPL_AUTHOR_EMAIL"] = p.Drone.Commit.Author.Email 341 | envs["TPL_AUTHOR_AVATAR"] = p.Drone.Commit.Author.Avatar 342 | 343 | envs["TPL_STATUS_PIC"] = p.getPicURL() 344 | envs["TPL_STATUS_COLOR"] = p.getColor() 345 | envs["TPL_STATUS_EMOTICON"] = p.getEmoticon() 346 | 347 | return envs 348 | } 349 | 350 | // getMessage get message tpl 351 | func (p *Plugin) getMessage() (tpl string, err error) { 352 | tpl, err = p.getTpl() 353 | if err != nil { 354 | return "", err 355 | } 356 | return p.fillTpl(tpl), nil 357 | } 358 | 359 | // getStatus 360 | func (p *Plugin) getStatus() string { 361 | if p.Drone.Build.Status == "success" { 362 | if p.Tpl.Build.Status.Success != "" { 363 | return p.Tpl.Build.Status.Success 364 | } 365 | 366 | return p.Drone.Build.Status 367 | } 368 | 369 | if p.Tpl.Build.Status.Failure != "" { 370 | return p.Tpl.Build.Status.Failure 371 | } 372 | 373 | return p.Drone.Build.Status 374 | } 375 | 376 | // get emoticon 377 | func (p *Plugin) getEmoticon() string { 378 | emoticons := make(map[string]string) 379 | emoticons["success"] = ":)" 380 | emoticons["failure"] = ":(" 381 | 382 | emoticon, ok := emoticons[p.Drone.Build.Status] 383 | if ok { 384 | return emoticon 385 | } 386 | 387 | return ":(" 388 | } 389 | 390 | // get picture url 391 | func (p *Plugin) getPicURL() string { 392 | pics := make(map[string]string) 393 | // success picture url 394 | pics["success"] = "https://wx1.sinaimg.cn/large/006tNc79gy1fz05g5a7utj30he0bfjry.jpg" 395 | if p.Custom.Pic.SuccessPicURL != "" { 396 | pics["success"] = p.Custom.Pic.SuccessPicURL 397 | } 398 | // failure picture url 399 | pics["failure"] = "https://wx1.sinaimg.cn/large/006tNc79gy1fz0b4fghpnj30hd0bdmxn.jpg" 400 | if p.Custom.Pic.FailurePicURL != "" { 401 | pics["failure"] = p.Custom.Pic.FailurePicURL 402 | } 403 | 404 | picURL, ok := pics[p.Drone.Build.Status] 405 | if ok { 406 | return picURL 407 | } 408 | 409 | return "" 410 | } 411 | 412 | // get color for message title 413 | func (p *Plugin) getColor() string { 414 | colors := make(map[string]string) 415 | // success color 416 | colors["success"] = "#008000" 417 | if p.Custom.Color.SuccessColor != "" { 418 | if p.Custom.Color.SuccessColor[0] != '#' { 419 | p.Custom.Color.SuccessColor = "#" + p.Custom.Color.SuccessColor 420 | } 421 | colors["success"] = p.Custom.Color.SuccessColor 422 | } 423 | 424 | // failure color 425 | colors["failure"] = "#FF0000" 426 | if p.Custom.Color.FailureColor != "" { 427 | if p.Custom.Color.FailureColor[0] != '#' { 428 | p.Custom.Color.FailureColor = "#" + p.Custom.Color.FailureColor 429 | } 430 | colors["failure"] = p.Custom.Color.FailureColor 431 | } 432 | 433 | color, ok := colors[p.Drone.Build.Status] 434 | if ok { 435 | return color 436 | } 437 | 438 | return "" 439 | } 440 | -------------------------------------------------------------------------------- /plugin_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestPlugin(t *testing.T) { 8 | p := Plugin{} 9 | err := p.Exec() 10 | if nil == err { 11 | t.Error("access token empty error should be catch!") 12 | } 13 | 14 | p.Config.AccessToken = "example-access-token" 15 | p.Custom.Tpl = "tpls/markdown.tpl" 16 | err = p.Exec() 17 | if nil == err { 18 | t.Error("not support message type error should be catch!") 19 | } 20 | 21 | p.Config.MsgType = "link" 22 | err = p.Exec() 23 | if nil == err { 24 | t.Error("access token invalid error should be catch!") 25 | } 26 | 27 | p.Custom.Tpl = "https://aaa.com" 28 | p.Config.MsgType = "text" 29 | err = p.Exec() 30 | if nil == err { 31 | t.Error("access token invalid error should be catch!") 32 | } 33 | 34 | 35 | p.Custom.Tpl = "" 36 | p.Config.MsgType = "link" 37 | err = p.Exec() 38 | if nil == err { 39 | t.Error("access token invalid error should be catch!") 40 | } 41 | 42 | p.Custom.Color.FailureColor = "#555555" 43 | p.Custom.Color.SuccessColor = "#222222" 44 | p.Custom.Pic.FailurePicURL = "https://www.baidu.com" 45 | p.Custom.Pic.SuccessPicURL = "https://www.baidu.com" 46 | p.Config.MsgType = "markdown" 47 | p.Custom.Tpl = "tpls/markdown.tpl" 48 | err = p.Exec() 49 | if nil == err { 50 | t.Error("access token invalid error should be catch!") 51 | } 52 | 53 | p.Custom.Tpl = "https://gist.githubusercontent.com/lddsb/87065e73678dcf56cd222a3c2f1f32b0/raw/fce9fb28b2c8c768eb93df5598beee8c98cba610/md.tpl" 54 | p.Drone.Build.Status = "failure" 55 | err = p.Exec() 56 | if nil == err { 57 | t.Error("access token invalid error should be catch!") 58 | } 59 | 60 | t.Log("plugin testing finished") 61 | } 62 | -------------------------------------------------------------------------------- /tpls/gitlab-ci.tpl: -------------------------------------------------------------------------------- 1 | ### **[CI_PROJECT_TITLE]**'s **[CI_COMMIT_BRANCH]** build **[TPL_BUILD_STATUS]** 2 | 3 | Message: [CI_COMMIT_MESSAGE] 4 | 5 | Detail: [[CI_COMMIT_SHA]]([CI_PROJECT_URL]/commit/[CI_COMMIT_SHA]) 6 | 7 | Author: [[GITLAB_USER_NAME]([GITLAB_USER_EMAIL])](mailto:[GITLAB_USER_EMAIL]) 8 | 9 | [Click To The Build Detail Page [TPL_STATUS_EMOTICON]]([CI_PIPELINE_URL]) 10 | -------------------------------------------------------------------------------- /tpls/markdown.tpl: -------------------------------------------------------------------------------- 1 | ### [TPL_REPO_SHORT_NAME] build [TPL_BUILD_STATUS] (`takes [TPL_BUILD_CONSUMING]s`) 2 | 3 | Message: [TPL_COMMIT_MSG] 4 | 5 | Detail: [[TPL_COMMIT_SHA]]([TPL_COMMIT_LINK]) 6 | 7 | Author: [[TPL_AUTHOR_NAME]([TPL_AUTHOR_EMAIL])](mailto:[TPL_AUTHOR_EMAIL]) 8 | 9 | [Click To The Build Detail Page [TPL_STATUS_EMOTICON]]([TPL_BUILD_LINK]) 10 | -------------------------------------------------------------------------------- /tpls/markdown_at_mobiles.tpl: -------------------------------------------------------------------------------- 1 | ### [TPL_REPO_SHORT_NAME] build [TPL_BUILD_STATUS] (`takes [TPL_BUILD_CONSUMING]s`) 2 | 3 | @mobile1 @mobile2 4 | 5 | Message: [TPL_COMMIT_MSG] 6 | 7 | Detail: [[TPL_COMMIT_SHA]]([TPL_COMMIT_LINK]) 8 | 9 | Author: [[TPL_AUTHOR_NAME]([TPL_AUTHOR_EMAIL])](mailto:[TPL_AUTHOR_EMAIL]) 10 | 11 | [Click To The Build Detail Page [TPL_STATUS_EMOTICON]]([TPL_BUILD_LINK]) 12 | -------------------------------------------------------------------------------- /tpls/text.tpl: -------------------------------------------------------------------------------- 1 | [TPL_REPO_NAME] build [TPL_BUILD_STATUS] (takes [TPL_BUILD_CONSUMING]s) 2 | [TPL_COMMIT_MSG] 3 | [TPL_COMMIT_SHA] ([TPL_COMMIT_LINK]) 4 | [TPL_AUTHOR_NAME] ([TPL_AUTHOR_EMAIL]) 5 | Click To The Build Detail Page [TPL_STATUS_EMOTICON] ([TPL_BUILD_LINK]) --------------------------------------------------------------------------------