├── .dockerignore ├── .drone.yml ├── .github ├── issue_template.md ├── pull_request_template.md └── settings.yml ├── .github_changelog_generator ├── .gitignore ├── .harness ├── eventPR.yaml ├── eventPush.yaml ├── eventTag.yaml └── harness.yaml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docker ├── Dockerfile.linux.amd64 ├── Dockerfile.linux.arm64 ├── Dockerfile.windows.1809 ├── Dockerfile.windows.ltsc2022 └── manifest.tmpl ├── go.mod ├── go.sum ├── main.go ├── plugin.go ├── plugin_test.go ├── renovate.json ├── templates ├── basic_fail.json ├── basic_on_hold.json ├── basic_success.json └── success_tag_deploy.json ├── test └── c.txt └── util.go /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !release/ 3 | -------------------------------------------------------------------------------- /.drone.yml: -------------------------------------------------------------------------------- 1 | kind: pipeline 2 | type: vm 3 | name: testing 4 | platform: 5 | os: linux 6 | arch: amd64 7 | pool: 8 | use: ubuntu 9 | 10 | steps: 11 | - name: lint 12 | image: golang:1.22 13 | pull: always 14 | commands: 15 | - go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest 16 | - golangci-lint version 17 | - golangci-lint run 18 | volumes: 19 | - name: gopath 20 | path: "/go" 21 | - name: test 22 | image: golang:1.21 23 | commands: 24 | - go test -cover ./... 25 | volumes: 26 | - name: gopath 27 | path: "/go" 28 | volumes: 29 | - name: gopath 30 | temp: {} 31 | trigger: 32 | ref: 33 | - refs/heads/master 34 | - refs/tags/** 35 | - refs/pull/** 36 | 37 | --- 38 | kind: pipeline 39 | type: vm 40 | name: linux-amd64 41 | platform: 42 | os: linux 43 | arch: amd64 44 | pool: 45 | use: ubuntu 46 | 47 | steps: 48 | - name: environment 49 | image: golang:1.21 50 | pull: always 51 | environment: 52 | CGO_ENABLED: "0" 53 | commands: 54 | - go version 55 | - go env 56 | - name: build 57 | image: golang:1.21 58 | environment: 59 | CGO_ENABLED: "0" 60 | commands: 61 | - go build -v -ldflags "-X main.version=" -a -tags netgo -o release/linux/amd64/drone-slack . 62 | - name: docker 63 | image: plugins/docker 64 | settings: 65 | dockerfile: docker/Dockerfile.linux.amd64 66 | repo: plugins/slack 67 | username: 68 | from_secret: docker_username 69 | password: 70 | from_secret: docker_password 71 | auto_tag: true 72 | auto_tag_suffix: linux-amd64 73 | depends_on: 74 | - testing 75 | trigger: 76 | ref: 77 | - refs/heads/master 78 | - refs/tags/** 79 | - refs/pull/** 80 | 81 | --- 82 | kind: pipeline 83 | type: vm 84 | name: linux-arm64 85 | platform: 86 | os: linux 87 | arch: arm64 88 | pool: 89 | use: ubuntu_arm64 90 | 91 | steps: 92 | - name: environment 93 | image: golang:1.21 94 | pull: always 95 | environment: 96 | CGO_ENABLED: "0" 97 | commands: 98 | - go version 99 | - go env 100 | - name: build 101 | image: golang:1.21 102 | environment: 103 | CGO_ENABLED: "0" 104 | commands: 105 | - go build -v -ldflags "-X main.version=" -a -tags netgo -o release/linux/arm64/drone-slack . 106 | - name: docker 107 | image: plugins/docker 108 | settings: 109 | dockerfile: docker/Dockerfile.linux.arm64 110 | repo: plugins/slack 111 | username: 112 | from_secret: docker_username 113 | password: 114 | from_secret: docker_password 115 | auto_tag: true 116 | auto_tag_suffix: linux-arm64 117 | depends_on: 118 | - testing 119 | trigger: 120 | ref: 121 | - refs/heads/master 122 | - refs/tags/** 123 | - refs/pull/** 124 | 125 | --- 126 | kind: pipeline 127 | type: vm 128 | name: windows-1809 129 | platform: 130 | os: windows 131 | arch: amd64 132 | pool: 133 | use: windows 134 | 135 | steps: 136 | - name: environment 137 | image: golang:1.21 138 | pull: always 139 | environment: 140 | CGO_ENABLED: "0" 141 | commands: 142 | - go version 143 | - go env 144 | - name: build 145 | image: golang:1.21 146 | environment: 147 | CGO_ENABLED: "0" 148 | commands: 149 | - go build -v -ldflags "-X main.version=" -a -tags netgo -o release/windows/amd64/drone-slack.exe . 150 | - name: docker 151 | image: plugins/docker 152 | settings: 153 | dockerfile: docker/Dockerfile.windows.1809 154 | repo: plugins/slack 155 | username: 156 | from_secret: docker_username 157 | password: 158 | from_secret: docker_password 159 | auto_tag: true 160 | auto_tag_suffix: windows-1809-amd64 161 | daemon_off: true 162 | purge: false 163 | when: 164 | ref: 165 | - refs/heads/master 166 | - refs/tags/** 167 | depends_on: 168 | - testing 169 | trigger: 170 | ref: 171 | - refs/heads/master 172 | - refs/tags/** 173 | - refs/pull/** 174 | 175 | --- 176 | kind: pipeline 177 | type: vm 178 | name: windows-ltsc2022 179 | platform: 180 | os: windows 181 | arch: amd64 182 | pool: 183 | use: windows-2022 184 | 185 | steps: 186 | - name: environment 187 | image: golang:1.21 188 | pull: always 189 | environment: 190 | CGO_ENABLED: "0" 191 | commands: 192 | - go version 193 | - go env 194 | - name: build 195 | image: golang:1.21 196 | environment: 197 | CGO_ENABLED: "0" 198 | commands: 199 | - go build -v -ldflags "-X main.version=" -a -tags netgo -o release/windows/amd64/drone-slack.exe . 200 | - name: docker 201 | image: plugins/docker 202 | settings: 203 | dockerfile: docker/Dockerfile.windows.ltsc2022 204 | repo: plugins/slack 205 | username: 206 | from_secret: docker_username 207 | password: 208 | from_secret: docker_password 209 | auto_tag: true 210 | auto_tag_suffix: windows-ltsc2022-amd64 211 | daemon_off: true 212 | purge: false 213 | when: 214 | ref: 215 | - refs/heads/master 216 | - refs/tags/** 217 | depends_on: 218 | - testing 219 | trigger: 220 | ref: 221 | - refs/heads/master 222 | - refs/tags/** 223 | - refs/pull/** 224 | 225 | --- 226 | kind: pipeline 227 | type: vm 228 | name: manifest 229 | platform: 230 | os: linux 231 | arch: amd64 232 | pool: 233 | use: ubuntu 234 | 235 | steps: 236 | - name: manifest 237 | image: plugins/manifest 238 | settings: 239 | auto_tag: "true" 240 | username: 241 | from_secret: docker_username 242 | password: 243 | from_secret: docker_password 244 | spec: docker/manifest.tmpl 245 | ignore_missing: true 246 | depends_on: 247 | - linux-amd64 248 | - linux-arm64 249 | - windows-1809 250 | - windows-ltsc2022 251 | trigger: 252 | ref: 253 | - refs/heads/master 254 | - refs/tags/** 255 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drone-plugins/drone-slack/7013a1e7b65e5bc75099ea9350b96da6decd2e8c/.github/pull_request_template.md -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | repository: 2 | name: drone-slack 3 | description: Drone plugin for sending Slack notifications 4 | homepage: http://plugins.drone.io/drone-plugins/drone-slack 5 | topics: drone, drone-plugin 6 | 7 | private: false 8 | has_issues: true 9 | has_wiki: false 10 | has_downloads: false 11 | 12 | default_branch: master 13 | 14 | allow_squash_merge: true 15 | allow_merge_commit: true 16 | allow_rebase_merge: true 17 | 18 | labels: 19 | - name: bug 20 | color: d73a4a 21 | description: Something isn't working 22 | - name: duplicate 23 | color: cfd3d7 24 | description: This issue or pull request already exists 25 | - name: enhancement 26 | color: a2eeef 27 | description: New feature or request 28 | - name: good first issue 29 | color: 7057ff 30 | description: Good for newcomers 31 | - name: help wanted 32 | color: 008672 33 | description: Extra attention is needed 34 | - name: invalid 35 | color: e4e669 36 | description: This doesn't seem right 37 | - name: question 38 | color: d876e3 39 | description: Further information is requested 40 | - name: renovate 41 | color: e99695 42 | description: Automated action from Renovate 43 | - name: wontfix 44 | color: ffffff 45 | description: This will not be worked on 46 | 47 | teams: 48 | - name: Admins 49 | permission: admin 50 | - name: Captain 51 | permission: admin 52 | - name: Maintainers 53 | permission: push 54 | 55 | branches: 56 | - name: master 57 | protection: 58 | required_pull_request_reviews: 59 | required_approving_review_count: 1 60 | dismiss_stale_reviews: false 61 | require_code_owner_reviews: false 62 | dismissal_restrictions: 63 | teams: 64 | - Admins 65 | - Captain 66 | required_status_checks: 67 | strict: true 68 | contexts: 69 | - continuous-integration/drone/pr 70 | enforce_admins: false 71 | restrictions: 72 | users: [] 73 | teams: [] 74 | -------------------------------------------------------------------------------- /.github_changelog_generator: -------------------------------------------------------------------------------- 1 | since-tag=v1.4.0 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | release/ 27 | vendor/ 28 | 29 | coverage.out 30 | drone-slack 31 | 32 | .idea 33 | -------------------------------------------------------------------------------- /.harness/eventPR.yaml: -------------------------------------------------------------------------------- 1 | inputSet: 2 | name: event-PR 3 | identifier: eventPR 4 | orgIdentifier: default 5 | projectIdentifier: Drone_Plugins 6 | pipeline: 7 | identifier: droneslackharness 8 | properties: 9 | ci: 10 | codebase: 11 | build: 12 | type: PR 13 | spec: 14 | number: <+trigger.prNumber> 15 | -------------------------------------------------------------------------------- /.harness/eventPush.yaml: -------------------------------------------------------------------------------- 1 | inputSet: 2 | name: event-Push 3 | identifier: eventPush 4 | orgIdentifier: default 5 | projectIdentifier: Drone_Plugins 6 | pipeline: 7 | identifier: droneslackharness 8 | properties: 9 | ci: 10 | codebase: 11 | build: 12 | type: branch 13 | spec: 14 | branch: <+trigger.branch> 15 | -------------------------------------------------------------------------------- /.harness/eventTag.yaml: -------------------------------------------------------------------------------- 1 | inputSet: 2 | name: event-Tag 3 | identifier: eventTag 4 | orgIdentifier: default 5 | projectIdentifier: Drone_Plugins 6 | pipeline: 7 | identifier: droneslackharness 8 | properties: 9 | ci: 10 | codebase: 11 | build: 12 | type: tag 13 | spec: 14 | tag: <+trigger.tag> 15 | -------------------------------------------------------------------------------- /.harness/harness.yaml: -------------------------------------------------------------------------------- 1 | pipeline: 2 | name: drone-slack-harness 3 | identifier: droneslackharness 4 | projectIdentifier: Drone_Plugins 5 | orgIdentifier: default 6 | tags: {} 7 | properties: 8 | ci: 9 | codebase: 10 | connectorRef: GitHub_Drone_Plugins_Org 11 | repoName: drone-slack 12 | build: <+input> 13 | sparseCheckout: [] 14 | stages: 15 | - stage: 16 | name: Testing Stage 17 | identifier: Testing_Stage 18 | type: CI 19 | spec: 20 | cloneCodebase: true 21 | caching: 22 | enabled: false 23 | paths: [] 24 | platform: 25 | os: Linux 26 | arch: Amd64 27 | runtime: 28 | type: Cloud 29 | spec: {} 30 | execution: 31 | steps: 32 | - step: 33 | type: Run 34 | name: Lint 35 | identifier: Lint 36 | spec: 37 | connectorRef: Plugins_Docker_Hub_Connector 38 | image: golang:1.22 39 | shell: Sh 40 | command: |- 41 | go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest 42 | golangci-lint version 43 | golangci-lint run 44 | - step: 45 | type: Run 46 | name: Test 47 | identifier: Run_1 48 | spec: 49 | connectorRef: Plugins_Docker_Hub_Connector 50 | image: golang:1.21 51 | shell: Sh 52 | command: go test -cover ./... 53 | description: "" 54 | - parallel: 55 | - stage: 56 | name: linux-amd64 57 | identifier: linuxamd64 58 | type: CI 59 | spec: 60 | cloneCodebase: true 61 | caching: 62 | enabled: false 63 | paths: [] 64 | platform: 65 | os: Linux 66 | arch: Amd64 67 | runtime: 68 | type: Cloud 69 | spec: {} 70 | execution: 71 | steps: 72 | - step: 73 | name: Build binaries 74 | identifier: Build_binaries 75 | type: Run 76 | spec: 77 | connectorRef: Plugins_Docker_Hub_Connector 78 | image: golang:1.21 79 | shell: Sh 80 | command: |- 81 | # force go modules 82 | export GOPATH="" 83 | 84 | # disable cgo 85 | export CGO_ENABLED=0 86 | 87 | set -e 88 | set -x 89 | 90 | # linux 91 | export GOOS=linux GOARCH=amd64 92 | go build -v -ldflags "-X main.build=<+pipeline.sequenceId>" -a -o release/linux/amd64/plugin 93 | when: 94 | stageStatus: Success 95 | - step: 96 | type: Plugin 97 | name: BuildAndPushDockerPlugin 98 | identifier: BuildAndPushDockerPlugin 99 | spec: 100 | connectorRef: Plugins_Docker_Hub_Connector 101 | image: plugins/docker 102 | settings: 103 | username: drone 104 | password: <+secrets.getValue("Plugins_Docker_Hub_Pat")> 105 | repo: plugins/slack 106 | dockerfile: docker/Dockerfile.linux.amd64 107 | auto_tag: "true" 108 | auto_tag_suffix: linux-amd64 109 | when: 110 | stageStatus: Success 111 | condition: <+codebase.build.type> == "tag" 112 | - step: 113 | type: BuildAndPushDockerRegistry 114 | name: BuildAndPushDockerRegistry 115 | identifier: BuildAndPushDockerRegistry 116 | spec: 117 | connectorRef: Plugins_Docker_Hub_Connector 118 | repo: plugins/slack 119 | tags: 120 | - linux-amd64 121 | caching: false 122 | dockerfile: docker/Dockerfile.linux.amd64 123 | when: 124 | stageStatus: Success 125 | condition: | 126 | <+codebase.build.type> == "branch" 127 | description: "" 128 | - stage: 129 | name: linux-arm64 130 | identifier: linuxarm64 131 | type: CI 132 | spec: 133 | cloneCodebase: true 134 | caching: 135 | enabled: false 136 | paths: [] 137 | platform: 138 | os: Linux 139 | arch: Arm64 140 | runtime: 141 | type: Cloud 142 | spec: {} 143 | execution: 144 | steps: 145 | - step: 146 | name: Build binaries 147 | identifier: Build_binaries 148 | type: Run 149 | spec: 150 | connectorRef: Plugins_Docker_Hub_Connector 151 | image: golang:1.21 152 | shell: Sh 153 | command: |- 154 | # force go modules 155 | export GOPATH="" 156 | 157 | # disable cgo 158 | export CGO_ENABLED=0 159 | 160 | set -e 161 | set -x 162 | 163 | # linux 164 | export GOOS=linux GOARCH=arm64 165 | go build -v -ldflags "-X main.version=" -a -tags netgo -o release/linux/arm64/drone-slack . 166 | when: 167 | stageStatus: Success 168 | - step: 169 | type: Plugin 170 | name: BuildAndPushDockerPlugin 171 | identifier: BuildAndPushDockerPlugin 172 | spec: 173 | connectorRef: Plugins_Docker_Hub_Connector 174 | image: plugins/docker 175 | settings: 176 | username: drone 177 | password: <+secrets.getValue("Plugins_Docker_Hub_Pat")> 178 | repo: plugins/slack 179 | dockerfile: docker/Dockerfile.linux.arm64 180 | auto_tag: "true" 181 | auto_tag_suffix: linux-arm64 182 | when: 183 | stageStatus: Success 184 | condition: <+codebase.build.type> == "tag" 185 | - step: 186 | type: BuildAndPushDockerRegistry 187 | name: BuildAndPushDockerRegistry 188 | identifier: BuildAndPushDockerRegistry 189 | spec: 190 | connectorRef: Plugins_Docker_Hub_Connector 191 | repo: plugins/slack 192 | tags: 193 | - linux-arm64 194 | caching: false 195 | dockerfile: docker/Dockerfile.linux.arm64 196 | when: 197 | stageStatus: Success 198 | condition: | 199 | <+codebase.build.type> == "branch" 200 | description: "" 201 | - stage: 202 | name: windows-1809-amd64 203 | identifier: windows1809amd64 204 | type: CI 205 | spec: 206 | cloneCodebase: true 207 | caching: 208 | enabled: false 209 | paths: [] 210 | execution: 211 | steps: 212 | - step: 213 | name: Build binaries 214 | identifier: Build_binaries 215 | type: Run 216 | spec: 217 | connectorRef: Plugins_Docker_Hub_Connector 218 | image: golang:1.21 219 | shell: Sh 220 | command: |- 221 | # force go modules 222 | export GOPATH="" 223 | 224 | # disable cgo 225 | export CGO_ENABLED=0 226 | 227 | set -e 228 | set -x 229 | 230 | # Windows 231 | GOOS=windows 232 | go build -v -ldflags "-X main.version=" -a -tags netgo -o release/windows/amd64/drone-slack.exe . 233 | when: 234 | stageStatus: Success 235 | - step: 236 | type: Plugin 237 | name: BuildAndPushDockerPlugin 238 | identifier: BuildAndPushDockerPlugin 239 | spec: 240 | connectorRef: Plugins_Docker_Hub_Connector 241 | image: plugins/docker 242 | settings: 243 | username: drone 244 | password: <+secrets.getValue("Plugins_Docker_Hub_Pat")> 245 | repo: plugins/slack 246 | dockerfile: docker/Dockerfile.windows.1809 247 | auto_tag: "true" 248 | auto_tag_suffix: windows-1809-amd64 249 | when: 250 | stageStatus: Success 251 | condition: <+codebase.build.type> == "tag" 252 | - step: 253 | type: BuildAndPushDockerRegistry 254 | name: BuildAndPushDockerRegistry 255 | identifier: BuildAndPushDockerRegistry 256 | spec: 257 | connectorRef: Plugins_Docker_Hub_Connector 258 | repo: plugins/slack 259 | tags: 260 | - windows-1809-amd64 261 | caching: false 262 | dockerfile: docker/Dockerfile.windows.1809 263 | when: 264 | stageStatus: Success 265 | condition: | 266 | <+codebase.build.type> == "branch" 267 | infrastructure: 268 | type: VM 269 | spec: 270 | type: Pool 271 | spec: 272 | poolName: windows-2019 273 | os: Windows 274 | description: "" 275 | delegateSelectors: 276 | - windows-vm 277 | - stage: 278 | name: windows-ltsc2022-amd64 279 | identifier: windowsltsc2022amd64 280 | type: CI 281 | spec: 282 | cloneCodebase: true 283 | caching: 284 | enabled: false 285 | paths: [] 286 | platform: 287 | os: Windows 288 | arch: Amd64 289 | runtime: 290 | type: Cloud 291 | spec: {} 292 | execution: 293 | steps: 294 | - step: 295 | name: Build binaries 296 | identifier: Build_binaries 297 | type: Run 298 | spec: 299 | connectorRef: Plugins_Docker_Hub_Connector 300 | image: golang:1.21 301 | shell: Sh 302 | command: |- 303 | # force go modules 304 | export GOPATH="" 305 | 306 | # disable cgo 307 | export CGO_ENABLED=0 308 | 309 | set -e 310 | set -x 311 | 312 | # Windows 313 | GOOS=windows 314 | go build -v -ldflags "-X main.version=" -a -tags netgo -o release/windows/amd64/drone-slack.exe . 315 | - step: 316 | type: Plugin 317 | name: BuildAndPushDockerPlugin 318 | identifier: BuildAndPushDockerPlugin 319 | spec: 320 | connectorRef: Plugins_Docker_Hub_Connector 321 | image: plugins/docker 322 | settings: 323 | username: drone 324 | password: <+secrets.getValue("Plugins_Docker_Hub_Pat")> 325 | repo: plugins/slack 326 | dockerfile: docker/Dockerfile.windows.ltsc2022 327 | auto_tag: "true" 328 | auto_tag_suffix: windows-ltsc2022-amd64 329 | when: 330 | stageStatus: Success 331 | condition: <+codebase.build.type> == "tag" 332 | - step: 333 | type: BuildAndPushDockerRegistry 334 | name: BuildAndPushDockerRegistry 335 | identifier: BuildAndPushDockerRegistry 336 | spec: 337 | connectorRef: Plugins_Docker_Hub_Connector 338 | repo: plugins/slack 339 | tags: 340 | - windows-ltsc2022-amd64 341 | caching: false 342 | dockerfile: docker/Dockerfile.windows.ltsc2022 343 | when: 344 | stageStatus: Success 345 | condition: | 346 | <+codebase.build.type> == "branch" 347 | description: "" 348 | - stage: 349 | name: Manifest 350 | identifier: Manifest 351 | type: CI 352 | spec: 353 | cloneCodebase: true 354 | caching: 355 | enabled: false 356 | paths: [] 357 | platform: 358 | os: Linux 359 | arch: Amd64 360 | runtime: 361 | type: Cloud 362 | spec: {} 363 | execution: 364 | steps: 365 | - step: 366 | type: Plugin 367 | name: Manifest 368 | identifier: Manifest 369 | spec: 370 | connectorRef: Plugins_Docker_Hub_Connector 371 | image: plugins/manifest 372 | settings: 373 | username: drone 374 | password: <+secrets.getValue("Plugins_Docker_Hub_Pat")> 375 | auto_tag: "true" 376 | ignore_missing: "true" 377 | spec: docker/manifest.tmpl 378 | when: 379 | stageStatus: Success 380 | condition: | 381 | <+codebase.build.type> == "tag" || <+codebase.build.type> == "branch" 382 | description: "" 383 | allowStageExecutions: true 384 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [v1.4.1](https://github.com/drone-plugins/drone-slack/tree/v1.4.1) (2021-11-09) 4 | 5 | [Full Changelog](https://github.com/drone-plugins/drone-slack/compare/v1.4.0...v1.4.1) 6 | 7 | **Closed issues:** 8 | 9 | - \[feature\] Allow template to be read from a file [\#122](https://github.com/drone-plugins/drone-slack/issues/122) 10 | - Newlines not working in message template [\#113](https://github.com/drone-plugins/drone-slack/issues/113) 11 | 12 | 13 | 14 | \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # drone-slack 2 | 3 | [![Build Status](http://harness.drone.io/api/badges/drone-plugins/drone-slack/status.svg)](http://harness.drone.io/drone-plugins/drone-slack) 4 | [![Slack](https://img.shields.io/badge/slack-drone-orange.svg?logo=slack)](https://join.slack.com/t/harnesscommunity/shared_invite/zt-y4hdqh7p-RVuEQyIl5Hcx4Ck8VCvzBw) 5 | [![Join the discussion at https://community.harness.io](https://img.shields.io/badge/discourse-forum-orange.svg)](https://community.harness.io) 6 | [![Drone questions at https://stackoverflow.com](https://img.shields.io/badge/drone-stackoverflow-orange.svg)](https://stackoverflow.com/questions/tagged/drone.io) 7 | [![Go Doc](https://godoc.org/github.com/drone-plugins/drone-slack?status.svg)](http://godoc.org/github.com/drone-plugins/drone-slack) 8 | [![Go Report](https://goreportcard.com/badge/github.com/drone-plugins/drone-slack)](https://goreportcard.com/report/github.com/drone-plugins/drone-slack) 9 | 10 | Drone plugin for sending Slack notifications. For the usage information and a listing of the available options please take a look at [the docs](http://plugins.drone.io/drone-plugins/drone-slack/). 11 | 12 | ## Build 13 | 14 | Build the binary with the following commands: 15 | 16 | ``` 17 | go build 18 | ``` 19 | 20 | ## Docker 21 | 22 | Build the Docker image with the following commands: 23 | 24 | ``` 25 | GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -a -tags netgo -o release/linux/amd64/drone-slack 26 | docker build --rm -t plugins/slack . 27 | ``` 28 | 29 | ## Send Slack messages Usage 30 | 31 | To Send Slack messages use the following 32 | 33 | Execute from the working directory: 34 | 35 | ``` 36 | docker run --rm \ 37 | -e SLACK_WEBHOOK=https://hooks.slack.com/services/... \ 38 | -e PLUGIN_CHANNEL=foo \ 39 | -e PLUGIN_USERNAME=drone \ 40 | -e DRONE_REPO_OWNER=octocat \ 41 | -e DRONE_REPO_NAME=hello-world \ 42 | -e DRONE_COMMIT_SHA=7fd1a60b01f91b314f59955a4e4d4e80d8edf11d \ 43 | -e DRONE_COMMIT_BRANCH=master \ 44 | -e DRONE_COMMIT_AUTHOR=octocat \ 45 | -e DRONE_COMMIT_AUTHOR_EMAIL=octocat@github.com \ 46 | -e DRONE_COMMIT_AUTHOR_AVATAR="https://avatars0.githubusercontent.com/u/583231?s=460&v=4" \ 47 | -e DRONE_COMMIT_AUTHOR_NAME="The Octocat" \ 48 | -e DRONE_BUILD_NUMBER=1 \ 49 | -e DRONE_BUILD_STATUS=success \ 50 | -e DRONE_BUILD_LINK=http://github.com/octocat/hello-world \ 51 | -e DRONE_TAG=1.0.0 \ 52 | plugins/slack 53 | ``` 54 | 55 | Please note the following new environment variables: 56 | 57 | - `SLACK_ACCESS_TOKEN`: The access token for Slack API authentication. 58 | - `PLUGIN_CUSTOM_BLOCK`: Custom blocks in JSON format to include in the Slack message. 59 | 60 | Make sure to replace `your_access_token` with your actual Slack access token and adjust 61 | 62 | If you provide an access token, it will use the Slack API to send the message. Otherwise, it will use the webhook. 63 | 64 | 65 | ## Upload files to Slack 66 | 67 | To Send Slack messages use the following 68 | 69 | Execute from the working directory: 70 | ``` 71 | docker run --network host --rm \ 72 | -e PLUGIN_ACCESS_TOKEN=your_access_token \ 73 | -e PLUGIN_CHANNEL=C07TL1KNV8Q \ 74 | -e PLUGIN_USERNAME=jenkinstest003app \ 75 | -e PLUGIN_FILE_PATH='/home/hns/test/b.txt' \ 76 | -e PLUGIN_INITIAL_COMMENT='some start of text' \ 77 | -e PLUGIN_TITLE='Build OK now' \ 78 | plugins/slack 79 | ``` 80 | 81 | Please note the following new environment variables: 82 | 83 | - `PLUGIN_ACCESS_TOKEN`: The access token for Slack API authentication. 84 | - `PLUGIN_CUSTOM_BLOCK`: Custom blocks in JSON format to include in the Slack message. 85 | 86 | Make sure to replace `your_access_token` with your actual Slack access token and adjust 87 | 88 | If you provide an access token, it will use the Slack API to send the message. 89 | 90 | 91 | ### Get Slack Id of a user from a Email ID 92 | ```bash 93 | docker run --network host --rm \ 94 | -e PLUGIN_ACCESS_TOKEN=your_access_token \ 95 | -e PLUGIN_SLACK_ID_OF=user01@somedomain.com \ 96 | plugins/slack 97 | ``` 98 | Output will be stored in the FOUND_SLACK_ID environment variable 99 | Make sure to replace `your_access_token` with your actual Slack access token and adjust 100 | 101 | ### Get the Slack IDs of all committers from a git repo with two commit ids as commit Ids 102 | ```bash 103 | docker run --network host --rm \ 104 | -e PLUGIN_ACCESS_TOKEN=your_access_token \ 105 | -e PLUGIN_COMMITTER_LIST_GIT_PATH=/harness \ 106 | -e PLUGIN_RECENT_COMMIT_ID=7fd1a60b01f91b314f59955a4e4d4e80d8edf11d \ 107 | -e PLUGIN_OLD_COMMIT_ID=7fd1a60b01f91b314f59955a4e4d4e80d8edf11d \ 108 | plugins/slack 109 | ``` 110 | 111 | ### Get the Slack IDs of all committers from a git repo with HEAD and a commit id 112 | ```bash 113 | docker run --network host --rm \ 114 | -e PLUGIN_ACCESS_TOKEN=your_access_token \ 115 | -e PLUGIN_COMMITTER_LIST_GIT_PATH=/harness \ 116 | -e PLUGIN_RECENT_COMMIT_ID=HEAD \ 117 | -e PLUGIN_OLD_COMMIT_ID=7fd1a60b01f91b314f59955a4e4d4e80d8edf11d \ 118 | plugins/slack 119 | ``` 120 | 121 | ### Get the Slack IDs of all committers from a git repo with HEAD and a number of commits behind HEAD 122 | ```bash 123 | docker run --network host --rm \ 124 | -e PLUGIN_ACCESS_TOKEN=your_access_token \ 125 | -e PLUGIN_COMMITTER_LIST_GIT_PATH=/harness \ 126 | -e PLUGIN_RECENT_COMMIT_ID=HEAD \ 127 | -e PLUGIN_OLD_COMMIT_ID=5 \ 128 | plugins/slack 129 | ``` 130 | 131 | Output will be stored in the COMMITTER_SLACK_ID_LIST environment variable as comma separated values. 132 | Make sure to replace `your_access_token` with your actual Slack access token and adjust. 133 | 134 | ## Release Preparation 135 | 136 | Run the changelog generator. 137 | 138 | ```BASH 139 | docker run -it --rm -v "$(pwd)":/usr/local/src/your-app githubchangeloggenerator/github-changelog-generator -u drone-plugins -p drone-slack -t 140 | ``` 141 | 142 | You can generate a token by logging into your GitHub account and going to Settings -> Personal access tokens. 143 | 144 | Next we tag the PR's with the fixes or enhancements labels. If the PR does not fufil the requirements, do not add a label. 145 | 146 | **Before moving on make sure to update the version file `version/version.go && version/version_test.go`.** 147 | 148 | Run the changelog generator again with the future version according to semver. 149 | 150 | ```BASH 151 | docker run -it --rm -v "$(pwd)":/usr/local/src/your-app githubchangeloggenerator/github-changelog-generator -u drone-plugins -p drone-slack --future-release v1.0.0 152 | ``` 153 | 154 | Create your pull request for the release. Get it merged then tag the release. 155 | -------------------------------------------------------------------------------- /docker/Dockerfile.linux.amd64: -------------------------------------------------------------------------------- 1 | FROM alpine:3.20 2 | 3 | LABEL maintainer="Drone.IO Community " \ 4 | org.label-schema.name="Drone Slack" \ 5 | org.label-schema.vendor="Drone.IO Community" \ 6 | org.label-schema.schema-version="1.0" 7 | 8 | ADD release/linux/amd64/drone-slack /bin/ 9 | ENTRYPOINT ["/bin/drone-slack"] 10 | -------------------------------------------------------------------------------- /docker/Dockerfile.linux.arm64: -------------------------------------------------------------------------------- 1 | FROM alpine:3.20 2 | 3 | LABEL maintainer="Drone.IO Community " \ 4 | org.label-schema.name="Drone Slack" \ 5 | org.label-schema.vendor="Drone.IO Community" \ 6 | org.label-schema.schema-version="1.0" 7 | 8 | ADD release/linux/arm64/drone-slack /bin/ 9 | ENTRYPOINT ["/bin/drone-slack"] 10 | -------------------------------------------------------------------------------- /docker/Dockerfile.windows.1809: -------------------------------------------------------------------------------- 1 | # escape=` 2 | FROM plugins/base:windows-1809-amd64@sha256:a5493cfd5ef8326296121233e392437ca535dcf8097f15edafd727fcf2d43ed6 3 | USER ContainerAdministrator 4 | 5 | ENV GODEBUG=netdns=go 6 | 7 | LABEL maintainer="Drone.IO Community " ` 8 | org.label-schema.name="Drone Slack" ` 9 | org.label-schema.vendor="Drone.IO Community" ` 10 | org.label-schema.schema-version="1.0" 11 | 12 | ADD release/windows/amd64/drone-slack.exe C:/drone-slack.exe 13 | ENTRYPOINT [ "C:\\drone-slack.exe" ] 14 | -------------------------------------------------------------------------------- /docker/Dockerfile.windows.ltsc2022: -------------------------------------------------------------------------------- 1 | # escape=` 2 | FROM plugins/base:windows-ltsc2022-amd64@sha256:0f90d5bceb432f1ee6f93cf44eed6a38c322834edd55df8a6648c9e6f15131f4 3 | USER ContainerAdministrator 4 | 5 | ENV GODEBUG=netdns=go 6 | 7 | LABEL maintainer="Drone.IO Community " ` 8 | org.label-schema.name="Drone Slack" ` 9 | org.label-schema.vendor="Drone.IO Community" ` 10 | org.label-schema.schema-version="1.0" 11 | 12 | ADD release/windows/amd64/drone-slack.exe C:/drone-slack.exe 13 | ENTRYPOINT [ "C:\\drone-slack.exe" ] -------------------------------------------------------------------------------- /docker/manifest.tmpl: -------------------------------------------------------------------------------- 1 | image: plugins/slack:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}} 2 | {{#if build.tags}} 3 | tags: 4 | {{#each build.tags}} 5 | - {{this}} 6 | {{/each}} 7 | {{/if}} 8 | manifests: 9 | - 10 | image: plugins/slack:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64 11 | platform: 12 | architecture: amd64 13 | os: linux 14 | - 15 | image: plugins/slack:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64 16 | platform: 17 | architecture: arm64 18 | os: linux 19 | variant: v8 20 | - 21 | image: plugins/slack:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}windows-1809-amd64 22 | platform: 23 | architecture: amd64 24 | os: windows 25 | version: 1809 26 | - 27 | image: plugins/slack:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}windows-ltsc2022-amd64 28 | platform: 29 | architecture: amd64 30 | os: windows 31 | version: ltsc2022 32 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/drone-plugins/drone-slack 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/drone/drone-template-lib v1.0.0 7 | github.com/go-git/go-git/v5 v5.12.0 8 | github.com/joho/godotenv v1.5.1 9 | github.com/slack-go/slack v0.15.0 10 | github.com/urfave/cli v1.22.14 11 | gotest.tools/v3 v3.5.1 12 | ) 13 | 14 | require ( 15 | dario.cat/mergo v1.0.0 // indirect 16 | github.com/Masterminds/goutils v1.1.0 // indirect 17 | github.com/Masterminds/semver v1.4.2 // indirect 18 | github.com/Masterminds/sprig v2.18.0+incompatible // indirect 19 | github.com/Microsoft/go-winio v0.6.1 // indirect 20 | github.com/ProtonMail/go-crypto v1.0.0 // indirect 21 | github.com/aymerick/raymond v2.0.2+incompatible // indirect 22 | github.com/cloudflare/circl v1.3.7 // indirect 23 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect 24 | github.com/cyphar/filepath-securejoin v0.2.4 // indirect 25 | github.com/emirpasic/gods v1.18.1 // indirect 26 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 27 | github.com/go-git/go-billy/v5 v5.5.0 // indirect 28 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 29 | github.com/google/go-cmp v0.6.0 // indirect 30 | github.com/google/uuid v1.1.0 // indirect 31 | github.com/gorilla/websocket v1.5.3 // indirect 32 | github.com/huandu/xstrings v1.2.0 // indirect 33 | github.com/imdario/mergo v0.3.7 // indirect 34 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 35 | github.com/kevinburke/ssh_config v1.2.0 // indirect 36 | github.com/pjbgf/sha1cd v0.3.0 // indirect 37 | github.com/pkg/errors v0.9.1 // indirect 38 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 39 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect 40 | github.com/skeema/knownhosts v1.2.2 // indirect 41 | github.com/xanzy/ssh-agent v0.3.3 // indirect 42 | golang.org/x/crypto v0.21.0 // indirect 43 | golang.org/x/mod v0.12.0 // indirect 44 | golang.org/x/net v0.22.0 // indirect 45 | golang.org/x/sys v0.18.0 // indirect 46 | golang.org/x/tools v0.13.0 // indirect 47 | gopkg.in/warnings.v0 v0.1.2 // indirect 48 | ) 49 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= 2 | dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 3 | github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 4 | github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg= 5 | github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= 6 | github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc= 7 | github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= 8 | github.com/Masterminds/sprig v2.18.0+incompatible h1:QoGhlbC6pter1jxKnjMFxT8EqsLuDE6FEcNbWEpw+lI= 9 | github.com/Masterminds/sprig v2.18.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= 10 | github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= 11 | github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= 12 | github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= 13 | github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= 14 | github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= 15 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= 16 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 17 | github.com/aymerick/raymond v2.0.2+incompatible h1:VEp3GpgdAnv9B2GFyTvqgcKvY+mfKMjPOA3SbKLtnU0= 18 | github.com/aymerick/raymond v2.0.2+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= 19 | github.com/bouk/monkey v1.0.0 h1:k6z8fLlPhETfn5l9rlWVE7Q6B23DoaqosTdArvNQRdc= 20 | github.com/bouk/monkey v1.0.0/go.mod h1:PG/63f4XEUlVyW1ttIeOJmJhhe1+t9EC/je3eTjvFhE= 21 | github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= 22 | github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= 23 | github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= 24 | github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= 25 | github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= 26 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 27 | github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= 28 | github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= 29 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 30 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 31 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 32 | github.com/drone/drone-template-lib v1.0.0 h1:PNBBfUhifRnrPCoWBlTitk3jipXdv8u8WLbIf7h7j00= 33 | github.com/drone/drone-template-lib v1.0.0/go.mod h1:Hqy1tgqPH5mtbFOZmow19l4jOkZvp+WZ00cB4W3MJhg= 34 | github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= 35 | github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= 36 | github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= 37 | github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= 38 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= 39 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= 40 | github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= 41 | github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= 42 | github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= 43 | github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= 44 | github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= 45 | github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho= 46 | github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= 47 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 48 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 49 | github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= 50 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 51 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 52 | github.com/google/uuid v1.1.0 h1:Jf4mxPC/ziBnoPIdpQdPJ9OeiomAUHLvxmPRSPH9m4s= 53 | github.com/google/uuid v1.1.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 54 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 55 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 56 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 57 | github.com/huandu/xstrings v1.2.0 h1:yPeWdRnmynF7p+lLYz0H2tthW9lqhMJrQV/U7yy4wX0= 58 | github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= 59 | github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI= 60 | github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 61 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 62 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 63 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 64 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 65 | github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= 66 | github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 67 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 68 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 69 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 70 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 71 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 72 | github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= 73 | github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= 74 | github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= 75 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 76 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 77 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 78 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 79 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 80 | github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= 81 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 82 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 83 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= 84 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= 85 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 86 | github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= 87 | github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= 88 | github.com/slack-go/slack v0.15.0 h1:LE2lj2y9vqqiOf+qIIy0GvEoxgF1N5yLGZffmEZykt0= 89 | github.com/slack-go/slack v0.15.0/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw= 90 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 91 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 92 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 93 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 94 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 95 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 96 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 97 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 98 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 99 | github.com/tkuchiki/faketime v0.0.0-20170607100027-a4500a4f4643 h1:ii/sHfgFMByozryLeiDmn1ClZ/Pena4NgpJ4P7UuX9o= 100 | github.com/tkuchiki/faketime v0.0.0-20170607100027-a4500a4f4643/go.mod h1:RXY/TXAwGGL36IKDjrHFMcjpUrEiyWSEtLhFPw3UWF0= 101 | github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk= 102 | github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA= 103 | github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= 104 | github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= 105 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 106 | golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 107 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 108 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 109 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 110 | golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= 111 | golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= 112 | golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= 113 | golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= 114 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 115 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 116 | golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= 117 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 118 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 119 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 120 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 121 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 122 | golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= 123 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 124 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 125 | golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= 126 | golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 127 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 128 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 129 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 130 | golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= 131 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 132 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 133 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 134 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 135 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 136 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 137 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 138 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 139 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 140 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 141 | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 142 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 143 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 144 | golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= 145 | golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 146 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 147 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 148 | golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= 149 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 150 | golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 151 | golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= 152 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 153 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 154 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 155 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 156 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 157 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 158 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 159 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 160 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 161 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 162 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 163 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 164 | golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= 165 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 166 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 167 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 168 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 169 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 170 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 171 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 172 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 173 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 174 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 175 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 176 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 177 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 178 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 179 | gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= 180 | gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= 181 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/joho/godotenv" 7 | "github.com/urfave/cli" 8 | "log" 9 | "os" 10 | ) 11 | 12 | var ( 13 | version = "0.0.0" 14 | build = "0" 15 | ) 16 | 17 | func main() { 18 | 19 | app := cli.NewApp() 20 | app.Name = "slack plugin" 21 | app.Usage = "slack plugin" 22 | app.Action = run 23 | app.Version = fmt.Sprintf("%s+%s", version, build) 24 | app.Flags = []cli.Flag{ 25 | cli.StringFlag{ 26 | Name: "webhook", 27 | Usage: "slack webhook url", 28 | EnvVar: "SLACK_WEBHOOK,PLUGIN_WEBHOOK", 29 | }, 30 | cli.StringFlag{ 31 | Name: "channel", 32 | Usage: "slack channel", 33 | EnvVar: "PLUGIN_CHANNEL", 34 | }, 35 | cli.StringFlag{ 36 | Name: "recipient", 37 | Usage: "slack recipient", 38 | EnvVar: "PLUGIN_RECIPIENT", 39 | }, 40 | cli.StringFlag{ 41 | Name: "username", 42 | Usage: "slack username", 43 | EnvVar: "PLUGIN_USERNAME", 44 | }, 45 | cli.StringFlag{ 46 | Name: "template", 47 | Usage: "slack template", 48 | EnvVar: "PLUGIN_TEMPLATE", 49 | }, 50 | cli.StringFlag{ 51 | Name: "fallback", 52 | Usage: "slack fallback", 53 | EnvVar: "PLUGIN_FALLBACK", 54 | }, 55 | cli.BoolFlag{ 56 | Name: "link-names", 57 | Usage: "slack link names", 58 | EnvVar: "PLUGIN_LINK_NAMES", 59 | }, 60 | cli.StringFlag{ 61 | Name: "image", 62 | Usage: "slack image url", 63 | EnvVar: "PLUGIN_IMAGE_URL", 64 | }, 65 | cli.StringFlag{ 66 | Name: "color", 67 | Usage: "slack color", 68 | EnvVar: "PLUGIN_COLOR", 69 | }, 70 | cli.StringFlag{ 71 | Name: "icon.url", 72 | Usage: "slack icon url", 73 | EnvVar: "PLUGIN_ICON_URL", 74 | }, 75 | cli.StringFlag{ 76 | Name: "icon.emoji", 77 | Usage: "slack emoji url", 78 | EnvVar: "PLUGIN_ICON_EMOJI", 79 | }, 80 | cli.StringFlag{ 81 | Name: "repo.owner", 82 | Usage: "repository owner", 83 | EnvVar: "DRONE_REPO_OWNER", 84 | }, 85 | cli.StringFlag{ 86 | Name: "repo.name", 87 | Usage: "repository name", 88 | EnvVar: "DRONE_REPO_NAME", 89 | }, 90 | cli.StringFlag{ 91 | Name: "commit.sha", 92 | Usage: "git commit sha", 93 | EnvVar: "DRONE_COMMIT_SHA", 94 | Value: "00000000", 95 | }, 96 | cli.StringFlag{ 97 | Name: "commit.ref", 98 | Value: "refs/heads/master", 99 | Usage: "git commit ref", 100 | EnvVar: "DRONE_COMMIT_REF", 101 | }, 102 | cli.StringFlag{ 103 | Name: "commit.branch", 104 | Value: "master", 105 | Usage: "git commit branch", 106 | EnvVar: "DRONE_COMMIT_BRANCH", 107 | }, 108 | cli.StringFlag{ 109 | Name: "commit.author", 110 | Usage: "git author username", 111 | EnvVar: "DRONE_COMMIT_AUTHOR", 112 | }, 113 | cli.StringFlag{ 114 | Name: "commit.author.email", 115 | Usage: "git author email", 116 | EnvVar: "DRONE_COMMIT_AUTHOR_EMAIL", 117 | }, 118 | cli.StringFlag{ 119 | Name: "commit.author.avatar", 120 | Usage: "git author avatar", 121 | EnvVar: "DRONE_COMMIT_AUTHOR_AVATAR", 122 | }, 123 | cli.StringFlag{ 124 | Name: "commit.author.name", 125 | Usage: "git author name", 126 | EnvVar: "DRONE_COMMIT_AUTHOR_NAME", 127 | }, 128 | cli.StringFlag{ 129 | Name: "commit.pull", 130 | Usage: "git pull request", 131 | EnvVar: "DRONE_PULL_REQUEST", 132 | }, 133 | cli.StringFlag{ 134 | Name: "commit.message", 135 | Usage: "commit message", 136 | EnvVar: "DRONE_COMMIT_MESSAGE", 137 | }, 138 | cli.StringFlag{ 139 | Name: "build.event", 140 | Value: "push", 141 | Usage: "build event", 142 | EnvVar: "DRONE_BUILD_EVENT", 143 | }, 144 | cli.IntFlag{ 145 | Name: "build.number", 146 | Usage: "build number", 147 | EnvVar: "DRONE_BUILD_NUMBER", 148 | }, 149 | cli.IntFlag{ 150 | Name: "build.parent", 151 | Usage: "build parent", 152 | EnvVar: "DRONE_BUILD_PARENT", 153 | }, 154 | cli.StringFlag{ 155 | Name: "build.status", 156 | Usage: "build status", 157 | Value: "success", 158 | EnvVar: "DRONE_BUILD_STATUS", 159 | }, 160 | cli.StringFlag{ 161 | Name: "build.link", 162 | Usage: "build link", 163 | EnvVar: "DRONE_BUILD_LINK", 164 | }, 165 | cli.Int64Flag{ 166 | Name: "build.started", 167 | Usage: "build started", 168 | EnvVar: "DRONE_BUILD_STARTED", 169 | }, 170 | cli.Int64Flag{ 171 | Name: "build.created", 172 | Usage: "build created", 173 | EnvVar: "DRONE_BUILD_CREATED", 174 | }, 175 | cli.StringFlag{ 176 | Name: "build.tag", 177 | Usage: "build tag", 178 | EnvVar: "DRONE_TAG", 179 | }, 180 | cli.StringFlag{ 181 | Name: "build.deployTo", 182 | Usage: "environment deployed to", 183 | EnvVar: "DRONE_DEPLOY_TO", 184 | }, 185 | cli.Int64Flag{ 186 | Name: "job.started", 187 | Usage: "job started", 188 | EnvVar: "DRONE_JOB_STARTED", 189 | }, 190 | cli.StringFlag{ 191 | Name: "custom.block", 192 | Usage: "custom block to send to slack. ", 193 | EnvVar: "PLUGIN_CUSTOM_BLOCK", 194 | }, 195 | cli.StringFlag{ 196 | Name: "access.token", 197 | Usage: "slack access token", 198 | EnvVar: "PLUGIN_ACCESS_TOKEN,SLACK_ACCESS_TOKEN", 199 | }, 200 | cli.StringFlag{ 201 | Name: "mentions", 202 | Usage: "slack mentions for the message.", 203 | EnvVar: "PLUGIN_MENTIONS", 204 | }, 205 | cli.StringFlag{ 206 | Name: "custom.template", 207 | Usage: "prebuilt custom template for the message.", 208 | EnvVar: "PLUGIN_CUSTOM_TEMPLATE", 209 | }, 210 | cli.StringFlag{ 211 | Name: "message", 212 | Usage: "slack message. either this or the custom template must be set. ", 213 | EnvVar: "PLUGIN_MESSAGE", 214 | }, 215 | 216 | // File send params 217 | cli.StringFlag{ 218 | Name: "filepath", 219 | Usage: "slack file path", 220 | EnvVar: "PLUGIN_FILE_PATH", 221 | }, 222 | cli.StringFlag{ 223 | Name: "filename", 224 | Usage: "slack file name", 225 | EnvVar: "PLUGIN_FILE_NAME", 226 | }, 227 | cli.StringFlag{ 228 | Name: "title", 229 | Usage: "slack title", 230 | EnvVar: "PLUGIN_TITLE", 231 | }, 232 | cli.StringFlag{ 233 | Name: "initial_comment", 234 | Usage: "slack initial comment", 235 | EnvVar: "PLUGIN_INITIAL_COMMENT", 236 | }, 237 | cli.BoolFlag{ 238 | Name: "fail_on_error", 239 | Usage: "fail build on error", 240 | EnvVar: "PLUGIN_FAIL_ON_ERROR", 241 | }, 242 | cli.StringFlag{ 243 | Name: "slack_id_of", 244 | Usage: "slack id required for the user email id", 245 | EnvVar: "PLUGIN_SLACK_USER_EMAIL_ID", 246 | }, 247 | cli.StringFlag{ 248 | Name: "committer_list_git_path", 249 | Usage: "git repo path holding the committers email id to fetch slack IDs from", 250 | EnvVar: "PLUGIN_GIT_REPO_PATH", 251 | }, 252 | cli.BoolFlag{ 253 | Name: "plugin_committer_slack_id", 254 | Usage: "flag to enable fetching slack IDs from the committers list", 255 | EnvVar: "PLUGIN_COMMITTERS_SLACK_ID", 256 | }, 257 | } 258 | 259 | if _, err := os.Stat("/run/drone/env"); err == nil { 260 | _ = godotenv.Overload("/run/drone/env") 261 | } 262 | 263 | if err := app.Run(os.Args); err != nil { 264 | log.Fatal(err) 265 | } 266 | } 267 | 268 | func run(c *cli.Context) error { 269 | plugin := Plugin{ 270 | Repo: Repo{ 271 | Owner: c.String("repo.owner"), 272 | Name: c.String("repo.name"), 273 | }, 274 | Build: Build{ 275 | Tag: c.String("build.tag"), 276 | Number: c.Int("build.number"), 277 | Parent: c.Int("build.parent"), 278 | Event: c.String("build.event"), 279 | Status: c.String("build.status"), 280 | Commit: c.String("commit.sha"), 281 | Ref: c.String("commit.ref"), 282 | Branch: c.String("commit.branch"), 283 | Author: Author{ 284 | Username: c.String("commit.author"), 285 | Name: c.String("commit.author.name"), 286 | Email: c.String("commit.author.email"), 287 | Avatar: c.String("commit.author.avatar"), 288 | }, 289 | Pull: c.String("commit.pull"), 290 | Message: newCommitMessage(c.String("commit.message")), 291 | DeployTo: c.String("build.deployTo"), 292 | Link: c.String("build.link"), 293 | Started: c.Int64("build.started"), 294 | Created: c.Int64("build.created"), 295 | }, 296 | Job: Job{ 297 | Started: c.Int64("job.started"), 298 | }, 299 | Config: Config{ 300 | Webhook: c.String("webhook"), 301 | Channel: c.String("channel"), 302 | Recipient: c.String("recipient"), 303 | Username: c.String("username"), 304 | Template: c.String("template"), 305 | Fallback: c.String("fallback"), 306 | ImageURL: c.String("image"), 307 | IconURL: c.String("icon.url"), 308 | IconEmoji: c.String("icon.emoji"), 309 | Color: c.String("color"), 310 | LinkNames: c.Bool("link-names"), 311 | CustomBlock: c.String("custom.block"), 312 | AccessToken: c.String("access.token"), 313 | Mentions: c.String("mentions"), 314 | CustomTemplate: c.String("custom.template"), 315 | Message: c.String("message"), 316 | // File upload attributes 317 | FilePath: c.String("filepath"), 318 | FileName: c.String("filename"), 319 | Title: c.String("title"), 320 | InitialComment: c.String("initial_comment"), 321 | FailOnError: c.Bool("fail_on_error"), 322 | SlackIdOf: c.String("slack_id_of"), 323 | CommitterListGitPath: c.String("committer_list_git_path"), 324 | CommitterSlackId: c.Bool("plugin_committer_slack_id"), 325 | }, 326 | } 327 | 328 | if plugin.Build.Commit == "" { 329 | plugin.Build.Commit = "0000000000000000000000000000000000000000" 330 | } 331 | if plugin.Config.Webhook == "" && plugin.Config.AccessToken == "" { 332 | return errors.New("you must provide a webhook url or access token") 333 | } 334 | 335 | return plugin.Exec() 336 | } 337 | -------------------------------------------------------------------------------- /plugin.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/go-git/go-git/v5/plumbing/object" 8 | "log" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | textTemplate "text/template" 13 | "time" 14 | 15 | "github.com/drone/drone-template-lib/template" 16 | "github.com/slack-go/slack" 17 | 18 | "github.com/go-git/go-git/v5" 19 | ) 20 | 21 | type ( 22 | Repo struct { 23 | Owner string 24 | Name string 25 | } 26 | 27 | BlockSet struct { 28 | Blocks []json.RawMessage `json:"blocks"` 29 | } 30 | 31 | Build struct { 32 | Tag string 33 | Event string 34 | Number int 35 | Parent int 36 | Commit string 37 | Ref string 38 | Branch string 39 | Author Author 40 | Pull string 41 | Message Message 42 | DeployTo string 43 | Status string 44 | Link string 45 | Started int64 46 | Created int64 47 | } 48 | 49 | Author struct { 50 | Username string 51 | Name string 52 | Email string 53 | Avatar string 54 | } 55 | 56 | Message struct { 57 | msg string 58 | Title string 59 | Body string 60 | } 61 | 62 | Config struct { 63 | Webhook string 64 | Channel string 65 | Recipient string 66 | Username string 67 | Template string 68 | Fallback string 69 | ImageURL string 70 | IconURL string 71 | IconEmoji string 72 | Color string 73 | LinkNames bool 74 | CustomBlock string 75 | AccessToken string 76 | Mentions string 77 | CustomTemplate string 78 | Message string 79 | // File Upload attributes 80 | FilePath string 81 | FileName string 82 | InitialComment string 83 | Title string 84 | FailOnError bool 85 | // Get Slack ID of the user by email 86 | SlackIdOf string 87 | // Git path to get list of committer emails 88 | CommitterListGitPath string 89 | CommitterSlackId bool 90 | } 91 | 92 | Job struct { 93 | Started int64 94 | } 95 | 96 | Plugin struct { 97 | Repo Repo 98 | Build Build 99 | Config Config 100 | Job Job 101 | } 102 | ) 103 | 104 | func (a Author) String() string { 105 | return a.Username 106 | } 107 | 108 | func newCommitMessage(m string) Message { 109 | splitMsg := strings.Split(m, "\n") 110 | 111 | return Message{ 112 | msg: m, 113 | Title: strings.TrimSpace(splitMsg[0]), 114 | Body: strings.TrimSpace(strings.Join(splitMsg[1:], "\n")), 115 | } 116 | } 117 | 118 | func (m Message) String() string { 119 | return m.msg 120 | } 121 | 122 | func (p Plugin) Exec() error { 123 | var blocks []slack.Block 124 | var channel string 125 | var text string 126 | var fallbackText string 127 | 128 | if p.Config.FilePath != "" { 129 | return p.UploadFile() 130 | } 131 | 132 | if p.Config.SlackIdOf != "" { 133 | return GetSlackIdFromEmail(&p) 134 | } 135 | 136 | if p.Config.CommitterSlackId && p.Config.Channel == "" { 137 | _, err := GetSlackIdsOfCommitters(&p, GetChangesetAuthorsList, getSlackUserIDByEmail) 138 | return err 139 | } 140 | 141 | // Determine the channel 142 | if p.Config.Recipient != "" { 143 | channel = prepend("@", p.Config.Recipient) 144 | } else if p.Config.Channel != "" { 145 | channel = prepend("#", p.Config.Channel) 146 | } 147 | 148 | // Determine the message and fallback 149 | if p.Config.Template != "" { 150 | var err error 151 | text, err = templateMessage(p.Config.Template, p) 152 | if err != nil { 153 | return err 154 | } 155 | } else if p.Config.Message != "" { 156 | text = p.Config.Message 157 | } else { 158 | text = message(p.Repo, p.Build) 159 | } 160 | 161 | // Add mentions to the message 162 | if p.Config.Mentions != "" { 163 | var mentionUserIDs = strings.Split(p.Config.Mentions, ",") 164 | mentions := make([]string, len(mentionUserIDs)) 165 | for i, id := range mentionUserIDs { 166 | // Check if the id starts with "@" and format it accordingly 167 | if strings.HasPrefix(id, "@") { 168 | mentions[i] = fmt.Sprintf("<%s>:", id) 169 | } else { 170 | mentions[i] = fmt.Sprintf("<@%s>:", id) 171 | } 172 | } 173 | mentionText := strings.Join(mentions, " ") 174 | text = fmt.Sprintf("%s %s", mentionText, text) 175 | } 176 | if p.Config.CustomTemplate != "" { 177 | // Read JSON from file 178 | var filePath string 179 | 180 | switch p.Config.CustomTemplate { 181 | case "basic_success_1": 182 | filePath = "templates/basic_success.json" 183 | case "basic_fail_1": 184 | filePath = "templates/basic_fail.json" 185 | case "success_tagged_deploy_1": 186 | filePath = "templates/success_tag_deploy.json" 187 | case "basic_on_hold_1": 188 | filePath = "templates/basic_on_hold.json" 189 | default: 190 | return fmt.Errorf("invalid template name: %s", p.Config.CustomTemplate) 191 | } 192 | 193 | // Read JSON from file 194 | file, err := os.ReadFile(filePath) 195 | if err != nil { 196 | return fmt.Errorf("failed to read template file: %w", err) 197 | } 198 | 199 | // Fill in the missing values in the template 200 | tmpl, err := textTemplate.New("template").Parse(string(file)) 201 | if err != nil { 202 | return fmt.Errorf("failed to parse template: %w", err) 203 | } 204 | 205 | var filledTemplate bytes.Buffer 206 | err = tmpl.Execute(&filledTemplate, p) 207 | if err != nil { 208 | return fmt.Errorf("failed to fill in template values: %w", err) 209 | } 210 | 211 | // Parse the filled template JSON into a BlockSet 212 | var blockSet BlockSet 213 | err = json.Unmarshal(filledTemplate.Bytes(), &blockSet) 214 | if err != nil { 215 | return fmt.Errorf("failed to parse filled template JSON: %w", err) 216 | } 217 | 218 | // Parse each block into a slack.Block and append to blocks 219 | for _, rawBlock := range blockSet.Blocks { 220 | var blockType struct { 221 | Type string `json:"type"` 222 | } 223 | err := json.Unmarshal(rawBlock, &blockType) 224 | if err != nil { 225 | return fmt.Errorf("failed to parse block type JSON: %w", err) 226 | } 227 | 228 | var block slack.Block 229 | switch blockType.Type { 230 | case "section": 231 | block = new(slack.SectionBlock) 232 | case "divider": 233 | block = new(slack.DividerBlock) 234 | case "header": 235 | block = new(slack.HeaderBlock) 236 | case "actions": 237 | block = new(slack.ActionBlock) 238 | default: 239 | return fmt.Errorf("unknown block type: %s", blockType.Type) 240 | } 241 | 242 | err = json.Unmarshal(rawBlock, block) 243 | if err != nil { 244 | return fmt.Errorf("failed to parse block JSON: %w", err) 245 | } 246 | 247 | blocks = append(blocks, block) 248 | } 249 | text = "" 250 | } 251 | 252 | if p.Config.Fallback != "" { 253 | var err error 254 | fallbackText, err = templateMessage(p.Config.Fallback, p) 255 | if err != nil { 256 | return err 257 | } 258 | } else { 259 | fallbackText = fallback(p.Repo, p.Build) 260 | } 261 | 262 | // Determine the color 263 | colorText := p.Config.Color 264 | if colorText == "" { 265 | colorText = color(p.Build) 266 | } 267 | 268 | // Parse custom blocks if they exist 269 | if p.Config.CustomBlock != "" { 270 | var blockSet BlockSet 271 | err := json.Unmarshal([]byte(p.Config.CustomBlock), &blockSet) 272 | if err != nil { 273 | return fmt.Errorf("could not unmarshal custom block: %w", err) 274 | } 275 | for _, rawMessage := range blockSet.Blocks { 276 | block := new(slack.SectionBlock) 277 | err := json.Unmarshal(rawMessage, block) 278 | if err != nil { 279 | return fmt.Errorf("could not unmarshal individual block: %w", err) 280 | } 281 | blocks = append(blocks, block) 282 | } 283 | } 284 | 285 | // If access token is provided, use it 286 | if p.Config.AccessToken != "" { 287 | slackApi := slack.New(p.Config.AccessToken) 288 | _, err := slackApi.AuthTest() 289 | if err != nil { 290 | return fmt.Errorf("failed to authenticate using access token: %w", err) 291 | } 292 | 293 | options := []slack.MsgOption{} 294 | if len(blocks) > 0 { 295 | options = append(options, slack.MsgOptionBlocks(blocks...)) 296 | } else { 297 | options = append(options, slack.MsgOptionText(text, false)) 298 | } 299 | 300 | _, _, err = slackApi.PostMessage(channel, options...) 301 | if err != nil { 302 | return fmt.Errorf("failed to post message using access token: %w", err) 303 | } 304 | 305 | if p.Config.CommitterSlackId { 306 | err := p.sendDirectMessageToCommitters(options) 307 | if err != nil { 308 | return fmt.Errorf("failed to send direct message to committers: %w", err) 309 | } 310 | } 311 | 312 | return nil 313 | } 314 | 315 | // Build the attachment 316 | attachment := slack.Attachment{ 317 | Color: colorText, 318 | ImageURL: p.Config.ImageURL, 319 | MarkdownIn: []string{"text", "fallback"}, 320 | Text: text, 321 | Fallback: fallbackText, 322 | } 323 | 324 | // Build the payload 325 | payload := slack.WebhookMessage{ 326 | Username: p.Config.Username, 327 | Attachments: []slack.Attachment{attachment}, 328 | IconURL: p.Config.IconURL, 329 | IconEmoji: p.Config.IconEmoji, 330 | Channel: channel, 331 | } 332 | 333 | // Add custom blocks to the payload if they exist 334 | if len(blocks) > 0 { 335 | payload.Blocks = &slack.Blocks{ 336 | BlockSet: blocks, 337 | } 338 | } 339 | 340 | // Post the message with the webhook 341 | return slack.PostWebhook(p.Config.Webhook, &payload) 342 | } 343 | 344 | func (p Plugin) UploadFile() error { 345 | 346 | p.Config.FilePath = strings.TrimSpace(p.Config.FilePath) 347 | 348 | api := slack.New(p.Config.AccessToken) 349 | fileSize, err := GetFileSize(p.Config.FilePath) 350 | if err != nil { 351 | log.Printf("Error getting file size: %s\n", err.Error()) 352 | return err 353 | } 354 | 355 | if p.Config.FileName == "" { 356 | fileName := filepath.Base(p.Config.FilePath) 357 | p.Config.FileName = fileName 358 | } 359 | 360 | params := slack.UploadFileV2Parameters{ 361 | File: p.Config.FilePath, 362 | Channel: p.Config.Channel, 363 | Filename: p.Config.FileName, 364 | Title: p.Config.Title, 365 | InitialComment: p.Config.InitialComment, 366 | FileSize: fileSize, 367 | } 368 | 369 | slackSummary, err := api.UploadFileV2(params) 370 | 371 | if !p.Config.FailOnError && slackSummary == nil { 372 | if err != nil { 373 | log.Println("Bad Api ret val, upload file failed but passing build as PLUGIN_FAIL_ON_ERROR is false") 374 | } 375 | return nil 376 | } else if p.Config.FailOnError && slackSummary == nil { 377 | log.Println("Bad ret val, Failed to upload file, failing build") 378 | _ = p.WriteFileUploadResult("", "", err) 379 | return fmt.Errorf("Bad ret val, Failed to upload file %s ", p.Config.FilePath) 380 | } 381 | 382 | if !p.Config.FailOnError && err != nil { 383 | if err != nil { 384 | log.Println("Unable to upload file but passing build PLUGIN_FAIL_ON_ERROR is false") 385 | } 386 | return nil 387 | } else if p.Config.FailOnError && err != nil { 388 | log.Println("Upload API Failed to upload file, failing build") 389 | _ = p.WriteFileUploadResult("", "", err) 390 | return fmt.Errorf("Failed to upload file %s ", p.Config.FilePath) 391 | } 392 | 393 | err = p.WriteFileUploadResult(slackSummary.ID, slackSummary.Title, err) 394 | if !p.Config.FailOnError { 395 | if err != nil { 396 | log.Println("Unable to Write output env var results for file upload " + 397 | "but passing build PLUGIN_FAIL_ON_ERROR is false") 398 | } 399 | return nil 400 | } 401 | 402 | return nil 403 | } 404 | 405 | func (p Plugin) WriteFileUploadResult(slackFileId, title string, err error) error { 406 | 407 | type EnvKvPair struct { 408 | Key string 409 | Value string 410 | } 411 | 412 | resultStr := "Failed: Slack file upload failed" 413 | if err == nil { 414 | resultStr = "Success: Slack file upload successful" 415 | } 416 | 417 | var kvPairs = []EnvKvPair{ 418 | {Key: "UPLOAD_OK_STATUS", Value: resultStr}, 419 | {Key: "UPLOAD_FILE_PATH", Value: p.Config.FilePath}, 420 | } 421 | 422 | var retErr error = nil 423 | 424 | for _, kvPair := range kvPairs { 425 | err := WriteEnvToOutputFile(kvPair.Key, kvPair.Value) 426 | if err != nil { 427 | retErr = err 428 | } 429 | } 430 | 431 | return retErr 432 | } 433 | 434 | func WriteEnvToOutputFile(key, value string) error { 435 | outputFile, err := os.OpenFile(os.Getenv("DRONE_OUTPUT"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 436 | if err != nil { 437 | return fmt.Errorf("failed to open output file: %w", err) 438 | } 439 | defer outputFile.Close() 440 | _, err = fmt.Fprintf(outputFile, "%s=%s\n", key, value) 441 | if err != nil { 442 | return fmt.Errorf("failed to write to env: %w", err) 443 | } 444 | return nil 445 | } 446 | 447 | func GetFileSize(filePath string) (int, error) { 448 | fileInfo, err := os.Stat(filePath) 449 | if err != nil { 450 | return 0, fmt.Errorf("failed to get file info: %w", err) 451 | } 452 | if fileInfo.IsDir() { 453 | return 0, fmt.Errorf("path %s is a directory, not a file", filePath) 454 | } 455 | return int(fileInfo.Size()), nil 456 | } 457 | 458 | func templateMessage(t string, plugin Plugin) (string, error) { 459 | c, err := contents(t) 460 | if err != nil { 461 | return "", fmt.Errorf("could not read template: %w", err) 462 | } 463 | 464 | return template.RenderTrim(c, plugin) 465 | } 466 | 467 | func message(repo Repo, build Build) string { 468 | return fmt.Sprintf("*%s* <%s|%s/%s#%s> (%s) by %s", 469 | build.Status, 470 | build.Link, 471 | repo.Owner, 472 | repo.Name, 473 | build.Commit[:8], 474 | build.Branch, 475 | build.Author, 476 | ) 477 | } 478 | 479 | func fallback(repo Repo, build Build) string { 480 | return fmt.Sprintf("%s %s/%s#%s (%s) by %s", 481 | build.Status, 482 | repo.Owner, 483 | repo.Name, 484 | build.Commit[:8], 485 | build.Branch, 486 | build.Author, 487 | ) 488 | } 489 | 490 | func color(build Build) string { 491 | switch build.Status { 492 | case "success": 493 | return "good" 494 | case "failure", "error", "killed": 495 | return "danger" 496 | default: 497 | return "warning" 498 | } 499 | } 500 | 501 | func prepend(prefix, s string) string { 502 | if !strings.HasPrefix(s, prefix) { 503 | return prefix + s 504 | } 505 | 506 | return s 507 | } 508 | 509 | func GetSlackIdsOfCommitters(p *Plugin, 510 | getAuthorsListFunc func(string) ([]string, error), 511 | getSlackUserIDByEmailFunc func(string, string) ([]string, error)) ([]string, error) { 512 | 513 | if p.Config.CommitterListGitPath == "" { 514 | p.Config.CommitterListGitPath = os.Getenv("DRONE_WORKSPACE") 515 | } 516 | 517 | emails, err := getAuthorsListFunc(p.Config.CommitterListGitPath) 518 | if err != nil { 519 | log.Println("Failed to get git emails: ", err) 520 | return []string{}, fmt.Errorf("failed to get git emails: %w", err) 521 | } 522 | 523 | slackUserIdList, err := getSlackUserIDByEmailFunc(p.Config.AccessToken, strings.Join(emails, ",")) 524 | if err != nil { 525 | log.Println("Failed to get Slack ID by email: ", err) 526 | return []string{}, fmt.Errorf("failed to get Slack ID by email: %w", err) 527 | } 528 | 529 | jsonStr := strings.Join(slackUserIdList, ",") 530 | err = WriteEnvToOutputFile("COMMITTERS_SLACK_IDS", jsonStr) 531 | if err != nil { 532 | log.Println("Failed to write git emails to output file: ", err) 533 | return []string{}, fmt.Errorf("failed to write git emails to output file: %w", err) 534 | } 535 | 536 | return slackUserIdList, nil 537 | } 538 | 539 | func GetSlackIdFromEmail(p *Plugin) error { 540 | slackIdList, err := getSlackUserIDByEmail(p.Config.AccessToken, p.Config.SlackIdOf) 541 | if err != nil { 542 | log.Println("Failed to get Slack ID by email: ", err) 543 | return fmt.Errorf("failed to get Slack ID by email: %w", err) 544 | } 545 | 546 | slackIdsCsvStr := strings.Join(slackIdList, ",") 547 | err = WriteEnvToOutputFile("SLACK_ID_FROM_EMAIL", slackIdsCsvStr) 548 | if err != nil { 549 | return fmt.Errorf("failed to write Slack ID to output file: %w", err) 550 | } 551 | return nil 552 | } 553 | 554 | func getSlackUserIDByEmail(accessToken, emailListStr string) ([]string, error) { 555 | 556 | emailArray := []string{} 557 | for _, email := range strings.Split(emailListStr, ",") { 558 | trimmedEmail := strings.TrimSpace(email) 559 | if trimmedEmail != "" { 560 | emailArray = append(emailArray, trimmedEmail) 561 | } 562 | } 563 | slackIdsList := []string{} 564 | 565 | var failedEmails []string 566 | for _, email := range emailArray { 567 | api := slack.New(accessToken) 568 | if api == nil { 569 | log.Println("Failed to create Slack client") 570 | return emailArray, fmt.Errorf("failed to create Slack client") 571 | } 572 | 573 | user, err := api.GetUserByEmail(email) 574 | if err != nil { 575 | log.Printf("Failed to fetch Slack ID for email %s: %v", email, err) 576 | failedEmails = append(failedEmails, email) 577 | continue 578 | } 579 | slackIdsList = append(slackIdsList, user.ID) 580 | 581 | // Add a short delay to avoid rate limits 582 | time.Sleep(500 * time.Millisecond) 583 | } 584 | if len(failedEmails) > 0 { 585 | log.Printf("Failed to fetch Slack IDs for the following emails: %v", failedEmails) 586 | } 587 | 588 | return slackIdsList, nil 589 | } 590 | 591 | func (p Plugin) sendDirectMessageToCommitters(options []slack.MsgOption) error { 592 | slackUserIdList, err := GetSlackIdsOfCommitters(&p, GetChangesetAuthorsList, getSlackUserIDByEmail) 593 | if err != nil { 594 | log.Println("Failed to get Slack ID by email: ", err) 595 | return fmt.Errorf("failed to get Slack ID by email: %w", err) 596 | } 597 | for _, slackUserId := range slackUserIdList { 598 | err = sendDirectMessage(p.Config.AccessToken, slackUserId, options) 599 | if err != nil { 600 | log.Println("Failed to send direct message: ", err) 601 | continue 602 | } 603 | log.Println("Message sent successfully for ", slackUserId) 604 | } 605 | return nil 606 | } 607 | 608 | func sendDirectMessage(botToken, userID string, options []slack.MsgOption) error { 609 | 610 | client := slack.New(botToken) 611 | 612 | channel, _, _, err := client.OpenConversation(&slack.OpenConversationParameters{ 613 | ReturnIM: true, 614 | Users: []string{userID}, 615 | }) 616 | if err != nil { 617 | log.Println("Failed to open conversation: ", err) 618 | } 619 | 620 | _, _, err = client.PostMessage(channel.ID, options...) 621 | if err != nil { 622 | log.Printf("Failed to send direct slack message: %v", err) 623 | } 624 | 625 | return nil 626 | } 627 | 628 | func GetChangesetAuthorsList(gitDir string) ([]string, error) { 629 | if gitDir == "" { 630 | log.Println("gitDir is empty") 631 | return nil, fmt.Errorf("gitDir cannot be empty") 632 | } 633 | 634 | absGitDir, err := filepath.Abs(gitDir) 635 | if err != nil { 636 | log.Println("Failed to get absolute path of gitDir: ", gitDir) 637 | return nil, fmt.Errorf("failed to get absolute path of gitDir: %w", err) 638 | } 639 | 640 | repo, err := git.PlainOpen(absGitDir) 641 | if err != nil { 642 | return nil, fmt.Errorf("failed to open git repository: %w", err) 643 | } 644 | 645 | headRef, err := repo.Head() 646 | if err != nil { 647 | return nil, fmt.Errorf("failed to get HEAD reference: %w", err) 648 | } 649 | 650 | headCommit, err := repo.CommitObject(headRef.Hash()) 651 | if err != nil { 652 | return nil, fmt.Errorf("failed to get HEAD commit: %w", err) 653 | } 654 | 655 | parentCommitIter := headCommit.Parents() 656 | oldCommit, err := parentCommitIter.Next() 657 | if err != nil { 658 | return nil, fmt.Errorf("failed to get parent commit of HEAD: %w", err) 659 | } 660 | 661 | emailSet := make(map[string]struct{}) 662 | 663 | commitIter, err := repo.Log(&git.LogOptions{ 664 | From: headCommit.Hash, 665 | Order: git.LogOrderCommitterTime, 666 | }) 667 | if err != nil { 668 | return nil, fmt.Errorf("failed to get commit log: %w", err) 669 | } 670 | 671 | err = commitIter.ForEach(func(commit *object.Commit) error { 672 | // Stop if we reach the parent commit 673 | if commit.Hash == oldCommit.Hash { 674 | return nil 675 | } 676 | 677 | email := strings.TrimSpace(commit.Author.Email) 678 | if email != "" { 679 | emailSet[email] = struct{}{} 680 | } 681 | return nil 682 | }) 683 | if err != nil { 684 | return nil, fmt.Errorf("error during commit log iteration: %w", err) 685 | } 686 | 687 | var uniqueEmails []string 688 | for email := range emailSet { 689 | uniqueEmails = append(uniqueEmails, email) 690 | } 691 | return uniqueEmails, nil 692 | } 693 | -------------------------------------------------------------------------------- /plugin_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/google/go-cmp/cmp" 6 | "io" 7 | "net/http" 8 | "net/http/httptest" 9 | "os" 10 | "strings" 11 | "testing" 12 | 13 | "gotest.tools/v3/assert" 14 | ) 15 | 16 | func TestExec(t *testing.T) { 17 | plugin := Plugin{ 18 | Repo: getTestRepo(), 19 | Build: getTestBuild(), 20 | Job: getTestJob(), 21 | Config: getTestConfig(), 22 | } 23 | 24 | handler := func(w http.ResponseWriter, r *http.Request) { 25 | out, _ := io.ReadAll(r.Body) 26 | got := string(out) 27 | want := `{"attachments":[{"color":"good","fallback":"Message Template Fallback:\nInitial commit\nmaster\nsuccess","text":"Message Template:\nInitial commit\n\nMessage body\nInitial commit\nMessage body","mrkdwn_in":["text","fallback"],"blocks":null}],"replace_original":false,"delete_original":false}` 28 | assert.Equal(t, got, want) 29 | } 30 | 31 | server := httptest.NewServer(http.HandlerFunc(handler)) 32 | defer server.Close() 33 | 34 | plugin.Config.Webhook = server.URL 35 | _ = plugin.Exec() 36 | } 37 | 38 | func TestNewCommitMessage(t *testing.T) { 39 | testCases := map[string]struct { 40 | Msg string 41 | Expect Message 42 | }{ 43 | "Empty Message": { 44 | Msg: "", 45 | Expect: Message{}, 46 | }, 47 | "Title Only": { 48 | Msg: "title with space", 49 | Expect: Message{ 50 | msg: "title with space", 51 | Title: "title with space", 52 | Body: "", 53 | }, 54 | }, 55 | "Title and Body": { 56 | Msg: "Title with space\nBody with space", 57 | Expect: Message{ 58 | msg: "Title with space\nBody with space", 59 | Title: "Title with space", 60 | Body: "Body with space", 61 | }, 62 | }, 63 | "Empty Second Line": { 64 | Msg: "Title with space\n\nBody with space", 65 | Expect: Message{ 66 | msg: "Title with space\n\nBody with space", 67 | Title: "Title with space", 68 | Body: "Body with space", 69 | }, 70 | }, 71 | } 72 | 73 | for name, testCase := range testCases { 74 | assert.Equal(t, testCase.Expect, newCommitMessage(testCase.Msg), name) 75 | } 76 | } 77 | 78 | func TestDefaultMessage(t *testing.T) { 79 | repo := getTestRepo() 80 | build := getTestBuild() 81 | 82 | msg := message(repo, build) 83 | expectedMessage := "*success* (master) by octocat" 84 | 85 | assert.Equal(t, expectedMessage, msg) 86 | } 87 | 88 | func TestDefaultFallbackMessage(t *testing.T) { 89 | repo := getTestRepo() 90 | build := getTestBuild() 91 | 92 | msg := fallback(repo, build) 93 | expectedMessage := "success octocat/hello-world#7fd1a60b (master) by octocat" 94 | 95 | assert.Equal(t, expectedMessage, msg) 96 | } 97 | 98 | func TestTemplateMessage(t *testing.T) { 99 | plugin := getTestPlugin() 100 | 101 | msg, err := templateMessage(plugin.Config.Template, plugin) 102 | assert.NilError(t, err, "should create message by template without error") 103 | expectedMessage := `Message Template: 104 | Initial commit 105 | 106 | Message body 107 | Initial commit 108 | Message body` 109 | 110 | assert.Equal(t, expectedMessage, msg) 111 | } 112 | 113 | func TestTemplateFallbackMessage(t *testing.T) { 114 | plugin := getTestPlugin() 115 | 116 | msg, err := templateMessage(plugin.Config.Fallback, plugin) 117 | assert.NilError(t, err, "should create message by template without error") 118 | expectedMessage := `Message Template Fallback: 119 | Initial commit 120 | master 121 | success` 122 | 123 | assert.Equal(t, expectedMessage, msg) 124 | } 125 | 126 | func getTestPlugin() Plugin { 127 | return Plugin{ 128 | Repo: getTestRepo(), 129 | Build: getTestBuild(), 130 | Config: getTestConfig(), 131 | } 132 | } 133 | 134 | func getTestRepo() Repo { 135 | return Repo{ 136 | Owner: "octocat", 137 | Name: "hello-world", 138 | } 139 | } 140 | 141 | func getTestBuild() Build { 142 | author := Author{ 143 | Username: "octocat", 144 | Name: "The Octocat", 145 | Email: "octocat@github.com", 146 | Avatar: "https://avatars0.githubusercontent.com/u/583231?s=460&v=4", 147 | } 148 | 149 | return Build{ 150 | Tag: "1.0.0", 151 | Event: "push", 152 | Number: 1, 153 | Commit: "7fd1a60b01f91b314f59955a4e4d4e80d8edf11d", 154 | Ref: "", 155 | Branch: "master", 156 | Author: author, 157 | Pull: "", 158 | Message: newCommitMessage("Initial commit\n\nMessage body"), 159 | DeployTo: "", 160 | Status: "success", 161 | Link: "http://github.com/octocat/hello-world", 162 | Started: 1546340400, // 2019-01-01 12:00:00 163 | Created: 1546340400, // 2019-01-01 12:00:00 164 | } 165 | } 166 | 167 | func getTestJob() Job { 168 | return Job{ 169 | Started: 1546340400, 170 | } 171 | } 172 | 173 | func getTestConfig() Config { 174 | t := `Message Template: 175 | {{build.message}} 176 | {{build.message.title}} 177 | {{build.message.body}}` 178 | 179 | tf := `Message Template Fallback: 180 | {{build.message.title}} 181 | {{build.branch}} 182 | {{build.status}}` 183 | 184 | return Config{ 185 | Template: t, 186 | Fallback: tf, 187 | } 188 | } 189 | 190 | func TestFileUpload(t *testing.T) { 191 | 192 | cfg := Config{ 193 | Channel: os.Getenv("PLUGIN_CHANNEL"), 194 | AccessToken: os.Getenv("PLUGIN_ACCESS_TOKEN"), 195 | FilePath: os.Getenv("PLUGIN_FILE_PATH"), 196 | FileName: os.Getenv("PLUGIN_FILE_NAME"), 197 | InitialComment: os.Getenv("PLUGIN_INITIAL_COMMENT"), 198 | Title: os.Getenv("PLUGIN_TITLE"), 199 | } 200 | 201 | plugin := Plugin{ 202 | Repo: getTestRepo(), 203 | Build: getTestBuild(), 204 | Job: getTestJob(), 205 | Config: cfg, 206 | } 207 | 208 | handler := func(w http.ResponseWriter, r *http.Request) { 209 | out, _ := io.ReadAll(r.Body) 210 | got := string(out) 211 | 212 | want := `{"attachments":[{"color":"good","fallback":"success octocat/hello-world#7fd1a60b (master) by octocat","text":"*success* \u003chttp://github.com/octocat/hello-world|octocat/hello-world#7fd1a60b\u003e (master) by octocat","mrkdwn_in":["text","fallback"],"blocks":null}],"replace_original":false,"delete_original":false}` 213 | assert.Equal(t, got, want) 214 | } 215 | 216 | server := httptest.NewServer(http.HandlerFunc(handler)) 217 | defer server.Close() 218 | 219 | plugin.Config.Webhook = server.URL 220 | 221 | _ = plugin.Exec() 222 | } 223 | 224 | func TestGetSlackIdFromEmail(t *testing.T) { 225 | config := Config{ 226 | AccessToken: "test-access-token", 227 | SlackIdOf: "octocat@github.com", 228 | } 229 | 230 | plugin := Plugin{ 231 | Config: config, 232 | } 233 | 234 | handler := func(w http.ResponseWriter, r *http.Request) { 235 | w.Header().Set("Content-Type", "application/json") 236 | response := `{ 237 | "user": { 238 | "id": "U12345", 239 | "name": "octocat" 240 | } 241 | }` 242 | _, _ = w.Write([]byte(response)) 243 | } 244 | 245 | server := httptest.NewServer(http.HandlerFunc(handler)) 246 | defer server.Close() 247 | 248 | plugin.Config.AccessToken = server.URL 249 | 250 | err := GetSlackIdFromEmail(&plugin) 251 | assert.NilError(t, err, "should retrieve Slack ID without error") 252 | } 253 | 254 | func TestGetSlackIdsOfCommitters(t *testing.T) { 255 | config := Config{ 256 | AccessToken: "mock-access-token", 257 | CommitterListGitPath: "/mock/repo/path", 258 | } 259 | 260 | plugin := Plugin{Config: config} 261 | 262 | mockGetAuthorsList := func(gitDir string) ([]string, error) { 263 | return []string{"user1@example.com", "user2@example.com"}, nil 264 | } 265 | 266 | mockGetSlackUserIDByEmail := func(accessToken string, emailList string) ([]string, error) { 267 | emails := strings.Split(emailList, ",") 268 | emailToID := map[string]string{ 269 | "user1@example.com": "U12345", 270 | "user2@example.com": "U67890", 271 | } 272 | 273 | var ids []string 274 | for _, email := range emails { 275 | id, ok := emailToID[email] 276 | if !ok { 277 | return nil, fmt.Errorf("invalid_auth") 278 | } 279 | ids = append(ids, id) 280 | } 281 | return ids, nil 282 | } 283 | 284 | slackIDs, err := GetSlackIdsOfCommitters(&plugin, mockGetAuthorsList, mockGetSlackUserIDByEmail) 285 | assert.NilError(t, err, "should retrieve Slack IDs without error") 286 | assert.DeepEqual(t, slackIDs, []string{"U12345", "U67890"}) 287 | } 288 | 289 | func TestGetSlackIdsOfCommitters_NoCommitters(t *testing.T) { 290 | config := Config{ 291 | AccessToken: "mock-access-token", 292 | CommitterListGitPath: "/mock/repo/path", 293 | } 294 | 295 | plugin := Plugin{Config: config} 296 | 297 | mockGetAuthorsList := func(gitDir string) ([]string, error) { 298 | return []string{}, nil 299 | } 300 | 301 | mockGetSlackUserIDByEmail := func(accessToken string, emailList string) ([]string, error) { 302 | if emailList == "" { 303 | return []string{}, nil 304 | } 305 | return nil, fmt.Errorf("unexpected call to getSlackUserIDByEmail") 306 | } 307 | 308 | slackIDs, err := GetSlackIdsOfCommitters(&plugin, mockGetAuthorsList, mockGetSlackUserIDByEmail) 309 | 310 | assert.NilError(t, err, "should handle a new repository without error") 311 | 312 | expectedSlackIDs := []string{} 313 | if diff := cmp.Diff(expectedSlackIDs, slackIDs); diff != "" { 314 | t.Errorf("mismatch in Slack IDs (-want +got):\n%s", diff) 315 | } 316 | } 317 | 318 | func TestGetSlackIdsOfCommitters_SlackUserLookupFailure(t *testing.T) { 319 | config := Config{ 320 | AccessToken: "mock-access-token", 321 | CommitterListGitPath: "/mock/repo/path", 322 | } 323 | 324 | plugin := Plugin{Config: config} 325 | mockGetAuthorsList := func(gitDir string) ([]string, error) { 326 | return []string{"user1@example.com", "user2@example.com"}, nil 327 | } 328 | 329 | mockGetSlackUserIDByEmail := func(accessToken string, emailList string) ([]string, error) { 330 | emails := strings.Split(emailList, ",") 331 | emailToID := map[string]string{ 332 | "user1@example.com": "U12345", 333 | } 334 | 335 | var ids []string 336 | for _, email := range emails { 337 | id, ok := emailToID[email] 338 | if !ok { 339 | return nil, fmt.Errorf("user lookup failed for email: %s", email) 340 | } 341 | ids = append(ids, id) 342 | } 343 | return ids, nil 344 | } 345 | 346 | slackIDs, err := GetSlackIdsOfCommitters(&plugin, mockGetAuthorsList, mockGetSlackUserIDByEmail) 347 | assert.ErrorContains(t, err, "user lookup failed for email: user2@example.com") 348 | expectedSlackIDs := []string{} 349 | if diff := cmp.Diff(expectedSlackIDs, slackIDs); diff != "" { 350 | t.Errorf("mismatch in Slack IDs (-want +got):\n%s", diff) 351 | } 352 | } 353 | 354 | func TestGetSlackIdsOfCommitters_SlackRateLimit(t *testing.T) { 355 | config := Config{ 356 | AccessToken: "mock-access-token", 357 | CommitterListGitPath: "/mock/repo/path", 358 | } 359 | 360 | plugin := Plugin{Config: config} 361 | mockGetAuthorsList := func(gitDir string) ([]string, error) { 362 | return []string{"user1@example.com", "user2@example.com"}, nil 363 | } 364 | 365 | mockGetSlackUserIDByEmail := func(accessToken string, emailList string) ([]string, error) { 366 | return nil, fmt.Errorf("rate_limit_exceeded: too many requests") 367 | } 368 | 369 | slackIDs, err := GetSlackIdsOfCommitters(&plugin, mockGetAuthorsList, mockGetSlackUserIDByEmail) 370 | 371 | assert.ErrorContains(t, err, "rate_limit_exceeded") 372 | expectedSlackIDs := []string{} 373 | if diff := cmp.Diff(expectedSlackIDs, slackIDs); diff != "" { 374 | t.Errorf("mismatch in Slack IDs (-want +got):\n%s", diff) 375 | } 376 | } 377 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "docker": { 6 | "fileMatch": [ 7 | "/docker/Dockerfile" 8 | ] 9 | }, 10 | "labels": [ 11 | "renovate" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /templates/basic_fail.json: -------------------------------------------------------------------------------- 1 | { 2 | "blocks": [ 3 | { 4 | "type": "header", 5 | "text": { 6 | "type": "plain_text", 7 | "text": "Build Failed. :red_circle:", 8 | "emoji": true 9 | } 10 | }, 11 | { 12 | "type": "section", 13 | "fields": [ 14 | { 15 | "type": "mrkdwn", 16 | "text": "*Project*: {{.Repo.Name}}" 17 | }, 18 | { 19 | "type": "mrkdwn", 20 | "text": "*Branch*: {{.Build.Branch}}" 21 | }, 22 | { 23 | "type": "mrkdwn", 24 | "text": "*Author*: {{.Build.Author.Username}}" 25 | } 26 | ] 27 | }, 28 | { 29 | "type": "actions", 30 | "elements": [ 31 | { 32 | "type": "button", 33 | "action_id": "view_build", 34 | "text": { 35 | "type": "plain_text", 36 | "text": "View Build" 37 | }, 38 | "url": "{{.Build.Link}}" 39 | } 40 | ] 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /templates/basic_on_hold.json: -------------------------------------------------------------------------------- 1 | { 2 | "blocks": [ 3 | { 4 | "type": "header", 5 | "text": { 6 | "type": "plain_text", 7 | "text": "ON HOLD - Awaiting Approval :raised_hand:", 8 | "emoji": true 9 | } 10 | }, 11 | { 12 | "type": "section", 13 | "fields": [ 14 | { 15 | "type": "mrkdwn", 16 | "text": "*Project*: {{.Repo.Name}}" 17 | }, 18 | { 19 | "type": "mrkdwn", 20 | "text": "*Branch*: {{.Build.Branch}}" 21 | }, 22 | { 23 | "type": "mrkdwn", 24 | "text": "*Author*: {{.Build.Author.Username}}" 25 | } 26 | ] 27 | }, 28 | { 29 | "type": "section", 30 | "fields": [ 31 | { 32 | "type": "mrkdwn", 33 | "text": "*Mentions*: {{.Config.Mentions}}" 34 | } 35 | ] 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /templates/basic_success.json: -------------------------------------------------------------------------------- 1 | { 2 | "blocks": [ 3 | { 4 | "type": "header", 5 | "text": { 6 | "type": "plain_text", 7 | "text": "Build Succeeded. :white_check_mark:", 8 | "emoji": true 9 | } 10 | }, 11 | { 12 | "type": "section", 13 | "fields": [ 14 | { 15 | "type": "mrkdwn", 16 | "text": "*Project*: {{.Repo.Name}}" 17 | }, 18 | { 19 | "type": "mrkdwn", 20 | "text": "*Branch*: {{.Build.Branch}}" 21 | }, 22 | { 23 | "type": "mrkdwn", 24 | "text": "*Author*: {{.Build.Author.Username}}" 25 | } 26 | ] 27 | }, 28 | { 29 | "type": "actions", 30 | "elements": [ 31 | { 32 | "type": "button", 33 | "action_id": "view_build", 34 | "text": { 35 | "type": "plain_text", 36 | "text": "View Build" 37 | }, 38 | "url": "{{.Build.Link}}" 39 | } 40 | ] 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /templates/success_tag_deploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "blocks": [ 3 | { 4 | "type": "header", 5 | "text": { 6 | "type": "plain_text", 7 | "text": "Deployment Successful! :tada:", 8 | "emoji": true 9 | } 10 | }, 11 | { 12 | "type": "section", 13 | "fields": [ 14 | { 15 | "type": "mrkdwn", 16 | "text": "*Project*: {{.Repo.Name}}" 17 | }, 18 | { 19 | "type": "mrkdwn", 20 | "text": "*When*: {{.Build.Started}}" 21 | "text": "*When*: {{.Build.Started}}" 22 | }, 23 | { 24 | "type": "mrkdwn", 25 | "text": "*Tag*: {{.Build.Tag}}" 26 | } 27 | ] 28 | }, 29 | { 30 | "type": "section", 31 | "fields": [ 32 | { 33 | "type": "mrkdwn", 34 | "text": "*Mentions*: {{.Config.Mentions}}" 35 | } 36 | ] 37 | }, 38 | { 39 | "type": "actions", 40 | "elements": [ 41 | { 42 | "type": "button", 43 | "action_id": "success_tagged_deploy_view", 44 | "text": { 45 | "type": "plain_text", 46 | "text": "View Job" 47 | }, 48 | "url": "{{.Build.Link}}" 49 | } 50 | ] 51 | } 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /test/c.txt: -------------------------------------------------------------------------------- 1 | slack file transfer contents 2 | apple peach orange 3 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "net/url" 8 | "os" 9 | ) 10 | 11 | func contents(str string) (string, error) { 12 | // Check for the empty string 13 | if str == "" { 14 | return str, nil 15 | } 16 | 17 | isFilePath := false 18 | 19 | // See if the string is referencing a URL 20 | if u, err := url.Parse(str); err == nil { 21 | switch u.Scheme { 22 | case "http", "https": 23 | res, err := http.Get(str) 24 | if err != nil { 25 | return "", err 26 | } 27 | 28 | defer res.Body.Close() 29 | b, err := io.ReadAll(res.Body) 30 | if err != nil { 31 | return "", fmt.Errorf("could not read response: %w", err) 32 | } 33 | 34 | return string(b), nil 35 | 36 | case "file": 37 | // Fall through to file loading 38 | str = u.Path 39 | isFilePath = true 40 | } 41 | } 42 | 43 | // See if the string is referencing a file 44 | _, err := os.Stat(str) 45 | if err == nil { 46 | b, err := os.ReadFile(str) 47 | if err != nil { 48 | return "", fmt.Errorf("could not load file %s: %w", str, err) 49 | } 50 | 51 | return string(b), nil 52 | } 53 | 54 | if isFilePath { 55 | return "", fmt.Errorf("could not load file %s: %w", str, err) 56 | } 57 | 58 | // Its a regular string 59 | return str, nil 60 | } 61 | --------------------------------------------------------------------------------