├── .github ├── settings.yml └── workflows │ ├── docker-promote.yml │ ├── feature-branch.yml │ └── release.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── dev.goreleaser.yaml ├── go.mod ├── images ├── slack-notification-example.png └── slack-notification-example2.png ├── main.go ├── renovate.json └── slack_notifier.go /.github/settings.yml: -------------------------------------------------------------------------------- 1 | # Upstream changes from _extends are only recognized when modifications are made to this file in the default branch. 2 | _extends: .github 3 | repository: 4 | name: slack-notifier 5 | description: Command line utility to send messages with attachments to Slack channels via Incoming Webhooks 6 | homepage: https://cloudposse.com/accelerate 7 | topics: "" 8 | -------------------------------------------------------------------------------- /.github/workflows/docker-promote.yml: -------------------------------------------------------------------------------- 1 | name: Docker Promote 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | permissions: 9 | contents: read 10 | packages: write 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | cancel-in-progress: false 15 | 16 | jobs: 17 | ci-docker: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Set output 21 | id: vars 22 | run: echo "tag=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT 23 | 24 | - uses: cloudposse/github-action-docker-promote@0.3.0 25 | id: promote 26 | with: 27 | registry: ghcr.io 28 | organization: "${{ github.event.repository.owner.login }}" 29 | repository: "${{ github.event.repository.name }}" 30 | login: "${{ github.actor }}" 31 | password: "${{ secrets.GITHUB_TOKEN }}" 32 | platforms: linux/amd64,linux/arm64 33 | from: sha-${{ github.sha }} 34 | to: ${{ steps.vars.outputs.tag }} 35 | use_metadata: false 36 | -------------------------------------------------------------------------------- /.github/workflows/feature-branch.yml: -------------------------------------------------------------------------------- 1 | name: Feature branch 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | pull_request: 7 | types: 8 | - opened 9 | - synchronize 10 | - reopened 11 | 12 | push: 13 | branches: 14 | - main 15 | - release/v* 16 | paths-ignore: 17 | - '.github/**' 18 | - 'docs/**' 19 | - 'examples/**' 20 | - 'test/**' 21 | 22 | permissions: 23 | contents: read 24 | packages: write 25 | 26 | concurrency: 27 | group: ${{ github.workflow }}-${{ github.ref }} 28 | cancel-in-progress: false 29 | 30 | jobs: 31 | ci-go: 32 | runs-on: ubuntu-latest 33 | steps: 34 | - name: Checkout 35 | uses: actions/checkout@v4 36 | with: 37 | fetch-depth: 0 38 | 39 | - name: Set up Go 40 | uses: actions/setup-go@v5 41 | with: 42 | go-version-file: go.mod 43 | cache: false 44 | 45 | - name: Test Snapshot Release 46 | uses: goreleaser/goreleaser-action@v5 47 | with: 48 | distribution: goreleaser 49 | version: latest 50 | args: release --config ./dev.goreleaser.yaml --clean --snapshot 51 | 52 | - name: Upload Test Release Assets 53 | uses: actions/upload-artifact@v4 54 | with: 55 | name: slack-notifier 56 | path: dist/* 57 | retention-days: 3 58 | 59 | ci-docker: 60 | runs-on: ubuntu-latest 61 | steps: 62 | - name: "Checkout source code at current commit" 63 | uses: actions/checkout@v4 64 | 65 | - name: Build 66 | id: build 67 | uses: cloudposse/github-action-docker-build-push@1.15.1 68 | with: 69 | registry: ghcr.io 70 | organization: "${{ github.event.repository.owner.login }}" 71 | repository: "${{ github.event.repository.name }}" 72 | login: "${{ github.actor }}" 73 | password: "${{ secrets.GITHUB_TOKEN }}" 74 | platforms: linux/amd64,linux/arm64 75 | 76 | release: 77 | if: github.event_name == 'push' 78 | needs: [ci-go, ci-docker] 79 | uses: cloudposse/.github/.github/workflows/shared-go-auto-release.yml@main 80 | with: 81 | publish: true 82 | format: binary 83 | secrets: inherit 84 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | release: 4 | types: [published] 5 | 6 | permissions: {} 7 | 8 | concurrency: 9 | group: ${{ github.workflow }} 10 | cancel-in-progress: false 11 | 12 | jobs: 13 | perform: 14 | uses: cloudposse/.github/.github/workflows/shared-release-branches.yml@main 15 | secrets: inherit 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | dist/bin/* 4 | slack-notifier 5 | .build-harness 6 | 7 | # Binaries for programs and plugins 8 | *.exe 9 | *.dll 10 | *.so 11 | *.dylib 12 | 13 | # Test binary, build with `go test -c` 14 | *.test 15 | 16 | # Output of the go coverage tool, specifically when used with LiteIDE 17 | *.out 18 | 19 | build-harness/ 20 | 21 | dist/ 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.22.2-bullseye as builder 2 | ENV GO111MODULE=on 3 | ENV CGO_ENABLED=0 4 | WORKDIR /usr/src/ 5 | COPY . /usr/src 6 | RUN go build -v -o "bin/slack-notifier" *.go 7 | 8 | FROM alpine:3.19 9 | RUN apk add --no-cache ca-certificates 10 | COPY --from=builder /usr/src/bin/* /usr/bin/ 11 | ENV PATH $PATH:/usr/bin 12 | ENTRYPOINT ["slack-notifier"] 13 | -------------------------------------------------------------------------------- /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 2018 Cloud Posse, LLC 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL = /bin/bash 2 | 3 | export DOCKER_ORG ?= cloudposse 4 | export DOCKER_IMAGE ?= $(DOCKER_ORG)/slack-notifier 5 | export DOCKER_TAG ?= latest 6 | export DOCKER_IMAGE_NAME ?= $(DOCKER_IMAGE):$(DOCKER_TAG) 7 | export DOCKER_BUILD_FLAGS = 8 | 9 | -include $(shell curl -sSL -o .build-harness "https://git.io/build-harness"; echo .build-harness) 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # slack-notifier 2 | 3 | 4 | Command line utility to send messages with [attachments](https://api.slack.com/docs/message-attachments) 5 | to [Slack](https://slack.com) channels via [Incoming Webhooks](https://api.slack.com/incoming-webhooks). 6 | 7 | 8 | ![GitHub Commit Status](images/slack-notification-example.png) 9 | 10 | 11 | ## Usage 12 | 13 | __NOTE__: The module accepts parameters as command-line arguments or as ENV variables 14 | (or any combination of command-line arguments and ENV vars). 15 | Command-line arguments take precedence over ENV vars. 16 | 17 | 18 | __NOTE__: The module supports up to 8 Fields in an [attachment](https://api.slack.com/docs/message-attachments) 19 | ### 20 | 21 | 22 | | Command-line argument | ENV var | Description | 23 | |:----------------------|:--------------------|:-------------------------------------------------------------------------------------------------------------------------------------| 24 | | webhook_url | SLACK_WEBHOOK_URL | Slack [Webhook URL](https://get.slack.help/hc/en-us/articles/115005265063-Incoming-WebHooks-for-Slack) | 25 | | user_name | SLACK_USER_NAME | Slack user name (the username from which the messages will be sent) | 26 | | icon_emoji | SLACK_ICON_EMOJI | Slack icon [emoji](https://www.webpagefx.com/tools/emoji-cheat-sheet) for the user's avatar | 27 | | fallback | SLACK_FALLBACK | A plain-text summary of the attachment. This text will be used in clients that don't show formatted text | 28 | | color | SLACK_COLOR | An optional value that can either be one of `good`, `warning`, `danger`, or a color code (_e.g._ `#439FE0`) | 29 | | channel | SLACK_CHANNEL | Slack channel to send to | 30 | | thread | SLACK_THREAD | Slack channel thread to send to | 31 | | pretext | SLACK_PRETEXT | Optional text that appears above the message attachment block | 32 | | author_name | SLACK_AUTHOR_NAME | Small text to display the attachment author's name | 33 | | author_link | SLACK_AUTHOR_LINK | URL that will hyperlink the author's name. Will only work if `author_name` is present | 34 | | author_icon | SLACK_AUTHOR_ICON | URL of a small 16x16px image to the left of the author's name. Will only work if `author_name` is present | 35 | | title | SLACK_TITLE | The `title` is displayed as larger, bold text near the top of a message attachment | 36 | | title_link | SLACK_TITLE_LINK | URL for the `title` text to be hyperlinked | 37 | | text | SLACK_TEXT | Main text in a message attachment | 38 | | thumb_url | SLACK_THUMB_URL | URL to an image file that will be displayed as a thumbnail on the right side of a message attachment | 39 | | footer | SLACK_FOOTER | Brief text to help contextualize and identify an attachment | 40 | | footer_icon | SLACK_FOOTER_ICON | URL of a small icon beside the `footer` text | 41 | | image_url | SLACK_IMAGE_URL | URL to an image file that will be displayed inside a message attachment | 42 | | field1_title | SLACK_FIELD1_TITLE | Field1 title | 43 | | field1_value | SLACK_FIELD1_VALUE | Field1 value | 44 | | field1_short | SLACK_FIELD1_SHORT | An optional boolean indicating whether the `value` is short enough to be displayed side-by-side with other values (default `false`) | 45 | | field2_title | SLACK_FIELD2_TITLE | Field2 title | 46 | | field2_value | SLACK_FIELD2_VALUE | Field2 value | 47 | | field2_short | SLACK_FIELD2_SHORT | An optional boolean indicating whether the `value` is short enough to be displayed side-by-side with other values (default `false`) | 48 | | field3_title | SLACK_FIELD3_TITLE | Field3 title | 49 | | field3_value | SLACK_FIELD3_VALUE | Field3 value | 50 | | field3_short | SLACK_FIELD3_SHORT | An optional boolean indicating whether the `value` is short enough to be displayed side-by-side with other values (default `false`) | 51 | | field4_title | SLACK_FIELD4_TITLE | Field4 title | 52 | | field4_value | SLACK_FIELD4_VALUE | Field4 value | 53 | | field4_short | SLACK_FIELD4_SHORT | An optional boolean indicating whether the `value` is short enough to be displayed side-by-side with other values (default `false`) | 54 | | field5_title | SLACK_FIELD5_TITLE | Field5 title | 55 | | field5_value | SLACK_FIELD5_VALUE | Field5 value | 56 | | field5_short | SLACK_FIELD5_SHORT | An optional boolean indicating whether the `value` is short enough to be displayed side-by-side with other values (default `false`) | 57 | | field6_title | SLACK_FIELD6_TITLE | Field6 title | 58 | | field6_value | SLACK_FIELD6_VALUE | Field6 value | 59 | | field6_short | SLACK_FIELD6_SHORT | An optional boolean indicating whether the `value` is short enough to be displayed side-by-side with other values (default `false`) | 60 | | field7_title | SLACK_FIELD7_TITLE | Field7 title | 61 | | field7_value | SLACK_FIELD7_VALUE | Field7 value | 62 | | field7_short | SLACK_FIELD7_SHORT | An optional boolean indicating whether the `value` is short enough to be displayed side-by-side with other values (default `false`) | 63 | | field8_title | SLACK_FIELD8_TITLE | Field8 title | 64 | | field8_value | SLACK_FIELD8_VALUE | Field8 value | 65 | | field8_short | SLACK_FIELD8_SHORT | An optional boolean indicating whether the `value` is short enough to be displayed side-by-side with other values (default `false`) | 66 | | field9_title | SLACK_FIELD9_TITLE | Field9 title | 67 | | field9_value | SLACK_FIELD9_VALUE | Field9 value | 68 | | field9_short | SLACK_FIELD9_SHORT | An optional boolean indicating whether the `value` is short enough to be displayed side-by-side with other values (default `false`) | 69 | | field10_title | SLACK_FIELD10_TITLE | Field10 title | 70 | | field10_value | SLACK_FIELD10_VALUE | Field10 value | 71 | | field10_short | SLACK_FIELD10_SHORT | An optional boolean indicating whether the `value` is short enough to be displayed side-by-side with other values (default `false`) | 72 | 73 | ### build the Go program locally 74 | 75 | ```sh 76 | go get 77 | 78 | CGO_ENABLED=0 go build -v -o "./dist/bin/slack-notifier" *.go 79 | ``` 80 | 81 | 82 | ### run locally with ENV vars 83 | 84 | ```sh 85 | export SLACK_WEBHOOK_URL="https://hooks.slack.com/services/XXXXXXXX/XXXXXXXX/XXXXXXXXXXXXXXXXXXXXXX" 86 | export SLACK_USER_NAME="CodeFresh" 87 | export SLACK_ICON_EMOJI=":white_check_mark:" 88 | export SLACK_FALLBACK="Deployed to Staging environment" 89 | export SLACK_COLOR="good" 90 | export SLACK_PRETEXT="Added XYZ to feature-104" 91 | export SLACK_AUTHOR_NAME="Auto Deploy Robot" 92 | export SLACK_AUTHOR_LINK="https://cloudposse.com/wp-content/uploads/sites/29/2018/02/small-cute-robot-square.png" 93 | export SLACK_AUTHOR_ICON="https://cloudposse.com/wp-content/uploads/sites/29/2018/02/small-cute-robot-square.png" 94 | export SLACK_TITLE="Environment Updated" 95 | export SLACK_TITLE_LINK="http://demo1.cloudposse.com" 96 | export SLACK_TEXT="The latest changes have been deployed" 97 | export SLACK_THUMB_URL="https://cloudposse.com/wp-content/uploads/sites/29/2018/02/SquareLogo2.png" 98 | export SLACK_FOOTER="Helm Deployment" 99 | export SLACK_FOOTER_ICON="https://cloudposse.com/wp-content/uploads/sites/29/2018/02/kubernetes.png" 100 | export SLACK_FIELD1_TITLE="Environment" 101 | export SLACK_FIELD1_VALUE="Staging" 102 | export SLACK_FIELD1_SHORT="true" 103 | export SLACK_FIELD2_TITLE="Namespace" 104 | export SLACK_FIELD2_VALUE="feature-104" 105 | export SLACK_FIELD2_SHORT="true" 106 | 107 | ./dist/bin/slack-notifier 108 | ``` 109 | 110 | 111 | ### run locally with command-line arguments 112 | 113 | ```sh 114 | ./dist/bin/slack-notifier \ 115 | -webhook_url "https://hooks.slack.com/services/XXXXXXXX/XXXXXXXX/XXXXXXXXXXXXXXXXXXXXXX" \ 116 | -user_name "CodeFresh" \ 117 | -icon_emoji ":white_check_mark:" \ 118 | -fallback "Deployed to Staging environment" \ 119 | -color "good" \ 120 | -pretext "Added XYZ to feature-104" \ 121 | -author_name "Auto Deploy Robot" \ 122 | -author_link "https://cloudposse.com/wp-content/uploads/sites/29/2018/02/small-cute-robot-square.png" \ 123 | -author_icon "https://cloudposse.com/wp-content/uploads/sites/29/2018/02/small-cute-robot-square.png" \ 124 | -title "Environment Updated" \ 125 | -title_link "http://demo1.cloudposse.com" \ 126 | -text "The latest changes have been deployed" \ 127 | -thumb_url "https://cloudposse.com/wp-content/uploads/sites/29/2018/02/SquareLogo2.png" \ 128 | -footer "Helm Deployment" \ 129 | -footer_icon "https://cloudposse.com/wp-content/uploads/sites/29/2018/02/kubernetes.png" \ 130 | -field1_title "Environment" \ 131 | -field1_value "Staging" \ 132 | -field1_short true \ 133 | -field2_title "Namespace" \ 134 | -field2_value "feature-104" \ 135 | -field2_short true 136 | ``` 137 | 138 | 139 | 140 | ### build the Docker image 141 | __NOTE__: it will download all `Go` dependencies and then build the program inside the container (see [`Dockerfile`](Dockerfile)) 142 | 143 | 144 | ```sh 145 | docker build --tag slack-notifier --no-cache=true . 146 | ``` 147 | 148 | 149 | 150 | ### run in a Docker container with ENV vars 151 | 152 | ```sh 153 | docker run -i --rm \ 154 | -e SLACK_WEBHOOK_URL="https://hooks.slack.com/services/XXXXXXXX/XXXXXXXX/XXXXXXXXXXXXXXXXXXXXXX" \ 155 | -e SLACK_USER_NAME="CodeFresh" \ 156 | -e SLACK_ICON_EMOJI=":white_check_mark:" \ 157 | -e SLACK_FALLBACK="Deployed to Staging environment" \ 158 | -e SLACK_COLOR="good" \ 159 | -e SLACK_PRETEXT="Added XYZ to feature-104" \ 160 | -e SLACK_AUTHOR_NAME="Auto Deploy Robot" \ 161 | -e SLACK_AUTHOR_LINK="https://cloudposse.com/wp-content/uploads/sites/29/2018/02/small-cute-robot-square.png" \ 162 | -e SLACK_AUTHOR_ICON="https://cloudposse.com/wp-content/uploads/sites/29/2018/02/small-cute-robot-square.png" \ 163 | -e SLACK_TITLE="Environment Updated" \ 164 | -e SLACK_TITLE_LINK="http://demo1.cloudposse.com" \ 165 | -e SLACK_TEXT="The latest changes have been deployed" \ 166 | -e SLACK_THUMB_URL="https://cloudposse.com/wp-content/uploads/sites/29/2018/02/SquareLogo2.png" \ 167 | -e SLACK_FOOTER="Helm Deployment" \ 168 | -e SLACK_FOOTER_ICON="https://cloudposse.com/wp-content/uploads/sites/29/2018/02/kubernetes.png" \ 169 | -e SLACK_FIELD1_TITLE="Environment" \ 170 | -e SLACK_FIELD1_VALUE="Staging" \ 171 | -e SLACK_FIELD1_SHORT="true" \ 172 | -e SLACK_FIELD2_TITLE="Namespace" \ 173 | -e SLACK_FIELD2_VALUE="feature-104" \ 174 | -e SLACK_FIELD2_SHORT="true" \ 175 | slack-notifier 176 | ``` 177 | 178 | 179 | 180 | ## References 181 | * https://get.slack.help/hc/en-us/articles/115005265063-Incoming-WebHooks-for-Slack 182 | * https://api.slack.com/incoming-webhooks 183 | * https://api.slack.com/docs/message-attachments 184 | * https://api.slack.com/docs/message-formatting 185 | * https://www.webpagefx.com/tools/emoji-cheat-sheet 186 | 187 | 188 | ## Help 189 | 190 | **Got a question?** 191 | 192 | File a GitHub [issue](https://github.com/cloudposse/slack-notifier/issues), send us an [email](mailto:hello@cloudposse.com) or reach out to us on [Gitter](https://gitter.im/cloudposse/). 193 | 194 | 195 | ## Contributing 196 | 197 | ### Bug Reports & Feature Requests 198 | 199 | Please use the [issue tracker](https://github.com/cloudposse/slack-notifier/issues) to report any bugs or file feature requests. 200 | 201 | ### Developing 202 | 203 | If you are interested in being a contributor and want to get involved in developing `slack-notifier`, we would love to hear from you! Shoot us an [email](mailto:hello@cloudposse.com). 204 | 205 | In general, PRs are welcome. We follow the typical "fork-and-pull" Git workflow. 206 | 207 | 1. **Fork** the repo on GitHub 208 | 2. **Clone** the project to your own machine 209 | 3. **Commit** changes to your own branch 210 | 4. **Push** your work back up to your fork 211 | 5. Submit a **Pull request** so that we can review your changes 212 | 213 | **NOTE:** Be sure to merge the latest from "upstream" before making a pull request! 214 | 215 | 216 | ## License 217 | 218 | [APACHE 2.0](LICENSE) © 2018 [Cloud Posse, LLC](https://cloudposse.com) 219 | 220 | See [LICENSE](LICENSE) for full details. 221 | 222 | Licensed to the Apache Software Foundation (ASF) under one 223 | or more contributor license agreements. See the NOTICE file 224 | distributed with this work for additional information 225 | regarding copyright ownership. The ASF licenses this file 226 | to you under the Apache License, Version 2.0 (the 227 | "License"); you may not use this file except in compliance 228 | with the License. You may obtain a copy of the License at 229 | 230 | http://www.apache.org/licenses/LICENSE-2.0 231 | 232 | Unless required by applicable law or agreed to in writing, 233 | software distributed under the License is distributed on an 234 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 235 | KIND, either express or implied. See the License for the 236 | specific language governing permissions and limitations 237 | under the License. 238 | 239 | 240 | ## About 241 | 242 | `slack-notifier` is maintained and funded by [Cloud Posse, LLC][website]. 243 | 244 | ![Cloud Posse](https://cloudposse.com/logo-300x69.png) 245 | 246 | 247 | Like it? Please let us know at 248 | 249 | We love [Open Source Software](https://github.com/cloudposse/)! 250 | 251 | See [our other projects][community] 252 | or [hire us][hire] to help build your next cloud platform. 253 | 254 | [website]: https://cloudposse.com/ 255 | [community]: https://github.com/cloudposse/ 256 | [hire]: https://cloudposse.com/contact/ 257 | 258 | 259 | ### Contributors 260 | 261 | | [![Erik Osterman][erik_img]][erik_web]
[Erik Osterman][erik_web] | [![Andriy Knysh][andriy_img]][andriy_web]
[Andriy Knysh][andriy_web] | 262 | |-------------------------------------------------------|------------------------------------------------------------------| 263 | 264 | [erik_img]: http://s.gravatar.com/avatar/88c480d4f73b813904e00a5695a454cb?s=144 265 | [erik_web]: https://github.com/osterman/ 266 | [andriy_img]: https://avatars0.githubusercontent.com/u/7356997?v=4&u=ed9ce1c9151d552d985bdf5546772e14ef7ab617&s=144 267 | [andriy_web]: https://github.com/aknysh/ 268 | -------------------------------------------------------------------------------- /dev.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | builds: 2 | - env: 3 | # goreleaser does not work with CGO, it could also complicate 4 | # usage by users in CI/CD systems like Terraform Cloud where 5 | # they are unable to install libraries. 6 | - CGO_ENABLED=0 7 | mod_timestamp: '{{ .CommitTimestamp }}' 8 | goos: 9 | - darwin 10 | - freebsd 11 | - windows 12 | - linux 13 | goarch: 14 | - amd64 15 | - '386' 16 | - arm 17 | - arm64 18 | ldflags: 19 | # -s Omit the symbol table and debug information 20 | # -w Omit the DWARF symbol table 21 | # -X importpath.name=value # set the value of the string variable in importpath named name to value 22 | # Someday we could implement a "version" command and set the version like this: 23 | # - '-s -w -X "github.com/cloudposse/slack-notifier/cmd.Version={{.Env.GORELEASER_CURRENT_TAG}}"' 24 | - '-s -w' 25 | 26 | archives: 27 | - format: binary 28 | name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}' 29 | 30 | checksum: 31 | name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS' 32 | algorithm: sha256 33 | 34 | release: 35 | # If you want to manually examine the release before it is live, uncomment this line: 36 | # draft: true 37 | 38 | changelog: 39 | skip: true -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cloudposse/slack-notifier 2 | 3 | go 1.20 4 | -------------------------------------------------------------------------------- /images/slack-notification-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudposse/slack-notifier/49c7a1a427370c82633368e9494008030a1ee2d9/images/slack-notification-example.png -------------------------------------------------------------------------------- /images/slack-notification-example2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudposse/slack-notifier/49c7a1a427370c82633368e9494008030a1ee2d9/images/slack-notification-example2.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "strconv" 9 | ) 10 | 11 | var ( 12 | webhookURL = flag.String("webhook_url", os.Getenv("SLACK_WEBHOOK_URL"), "Slack Webhook URL") 13 | userName = flag.String("user_name", os.Getenv("SLACK_USER_NAME"), "Slack user name (the username from which the messages will be sent)") 14 | iconEmoji = flag.String("icon_emoji", os.Getenv("SLACK_ICON_EMOJI"), "Slack icon emoji for the user's avatar. https://www.webpagefx.com/tools/emoji-cheat-sheet") 15 | fallback = flag.String("fallback", os.Getenv("SLACK_FALLBACK"), "A plain-text summary of the attachment. This text will be used in clients that don't show formatted text") 16 | color = flag.String("color", os.Getenv("SLACK_COLOR"), "An optional value that can either be one of good, warning, danger, or any hex color code (e.g. #439FE0)") 17 | channel = flag.String("channel", os.Getenv("SLACK_CHANNEL"), "Slack channel to send to") 18 | thread = flag.String("thread", os.Getenv("SLACK_THREAD"), "Slack channel thread to send to") 19 | pretext = flag.String("pretext", os.Getenv("SLACK_PRETEXT"), "Optional text that appears above the message attachment block") 20 | authorName = flag.String("author_name", os.Getenv("SLACK_AUTHOR_NAME"), "Small text to display the attachment author's name") 21 | authorLink = flag.String("author_link", os.Getenv("SLACK_AUTHOR_LINK"), "URL that will hyperlink the author's name. Will only work if author_name is present") 22 | authorIcon = flag.String("author_icon", os.Getenv("SLACK_AUTHOR_ICON"), "URL of a small 16x16px image to the left of the author's name. Will only work if `author_name` is present") 23 | title = flag.String("title", os.Getenv("SLACK_TITLE"), "The title is displayed as larger, bold text near the top of a message attachment") 24 | titleLink = flag.String("title_link", os.Getenv("SLACK_TITLE_LINK"), "URL for the title text to be hyperlinked") 25 | text = flag.String("text", os.Getenv("SLACK_TEXT"), "Main text in a message attachment") 26 | thumbURL = flag.String("thumb_url", os.Getenv("SLACK_THUMB_URL"), "URL to an image file that will be displayed as a thumbnail on the right side of a message attachment") 27 | footer = flag.String("footer", os.Getenv("SLACK_FOOTER"), "Brief text to help contextualize and identify an attachment") 28 | footerIcon = flag.String("footer_icon", os.Getenv("SLACK_FOOTER_ICON"), "URL of a small icon beside the footer text") 29 | imageURL = flag.String("image_url", os.Getenv("SLACK_IMAGE_URL"), "URL to an image file that will be displayed inside a message attachment") 30 | field1Title = flag.String("field1_title", os.Getenv("SLACK_FIELD1_TITLE"), "Field1 title") 31 | field1Value = flag.String("field1_value", os.Getenv("SLACK_FIELD1_VALUE"), "Field1 value") 32 | field1Short = flag.String("field1_short", os.Getenv("SLACK_FIELD1_SHORT"), "An optional boolean indicating whether the 'value' is short enough to be displayed side-by-side with other values (default 'false')") 33 | field2Title = flag.String("field2_title", os.Getenv("SLACK_FIELD2_TITLE"), "Field2 title") 34 | field2Value = flag.String("field2_value", os.Getenv("SLACK_FIELD2_VALUE"), "Field2 value") 35 | field2Short = flag.String("field2_short", os.Getenv("SLACK_FIELD2_SHORT"), "An optional boolean indicating whether the 'value' is short enough to be displayed side-by-side with other values (default 'false')") 36 | field3Title = flag.String("field3_title", os.Getenv("SLACK_FIELD3_TITLE"), "Field3 title") 37 | field3Value = flag.String("field3_value", os.Getenv("SLACK_FIELD3_VALUE"), "Field3 value") 38 | field3Short = flag.String("field3_short", os.Getenv("SLACK_FIELD3_SHORT"), "An optional boolean indicating whether the 'value' is short enough to be displayed side-by-side with other values (default 'false')") 39 | field4Title = flag.String("field4_title", os.Getenv("SLACK_FIELD4_TITLE"), "Field4 title") 40 | field4Value = flag.String("field4_value", os.Getenv("SLACK_FIELD4_VALUE"), "Field4 value") 41 | field4Short = flag.String("field4_short", os.Getenv("SLACK_FIELD4_SHORT"), "An optional boolean indicating whether the 'value' is short enough to be displayed side-by-side with other values (default 'false')") 42 | field5Title = flag.String("field5_title", os.Getenv("SLACK_FIELD5_TITLE"), "Field5 title") 43 | field5Value = flag.String("field5_value", os.Getenv("SLACK_FIELD5_VALUE"), "Field5 value") 44 | field5Short = flag.String("field5_short", os.Getenv("SLACK_FIELD5_SHORT"), "An optional boolean indicating whether the 'value' is short enough to be displayed side-by-side with other values (default 'false')") 45 | field6Title = flag.String("field6_title", os.Getenv("SLACK_FIELD6_TITLE"), "Field6 title") 46 | field6Value = flag.String("field6_value", os.Getenv("SLACK_FIELD6_VALUE"), "Field6 value") 47 | field6Short = flag.String("field6_short", os.Getenv("SLACK_FIELD6_SHORT"), "An optional boolean indicating whether the 'value' is short enough to be displayed side-by-side with other values (default 'false')") 48 | field7Title = flag.String("field7_title", os.Getenv("SLACK_FIELD7_TITLE"), "Field7 title") 49 | field7Value = flag.String("field7_value", os.Getenv("SLACK_FIELD7_VALUE"), "Field7 value") 50 | field7Short = flag.String("field7_short", os.Getenv("SLACK_FIELD7_SHORT"), "An optional boolean indicating whether the 'value' is short enough to be displayed side-by-side with other values (default 'false')") 51 | field8Title = flag.String("field8_title", os.Getenv("SLACK_FIELD8_TITLE"), "Field8 title") 52 | field8Value = flag.String("field8_value", os.Getenv("SLACK_FIELD8_VALUE"), "Field8 value") 53 | field8Short = flag.String("field8_short", os.Getenv("SLACK_FIELD8_SHORT"), "An optional boolean indicating whether the 'value' is short enough to be displayed side-by-side with other values (default 'false')") 54 | field9Title = flag.String("field9_title", os.Getenv("SLACK_FIELD9_TITLE"), "Field9 title") 55 | field9Value = flag.String("field9_value", os.Getenv("SLACK_FIELD9_VALUE"), "Field9 value") 56 | field9Short = flag.String("field9_short", os.Getenv("SLACK_FIELD9_SHORT"), "An optional boolean indicating whether the 'value' is short enough to be displayed side-by-side with other values (default 'false')") 57 | field10Title = flag.String("field10_title", os.Getenv("SLACK_FIELD10_TITLE"), "Field10 title") 58 | field10Value = flag.String("field10_value", os.Getenv("SLACK_FIELD10_VALUE"), "Field10 value") 59 | field10Short = flag.String("field10_short", os.Getenv("SLACK_FIELD10_SHORT"), "An optional boolean indicating whether the 'value' is short enough to be displayed side-by-side with other values (default 'false')") 60 | ) 61 | 62 | func addField(fields []Field, fieldTitle string, fieldValue string, fieldShort string) []Field { 63 | if fieldTitle != "" && fieldValue != "" { 64 | var short = false 65 | var err error 66 | 67 | if fieldShort != "" { 68 | if short, err = strconv.ParseBool(fieldShort); err != nil { 69 | fmt.Println("slack-notifier: Error: ", err.Error()) 70 | short = false 71 | } 72 | } 73 | 74 | fields = append(fields, Field{ 75 | Title: fieldTitle, 76 | Value: fieldValue, 77 | Short: short, 78 | }) 79 | } 80 | 81 | return fields 82 | } 83 | 84 | func main() { 85 | flag.Parse() 86 | 87 | if *webhookURL == "" { 88 | flag.PrintDefaults() 89 | log.Fatal("-webhook_url or SLACK_WEBHOOK_URL required") 90 | } 91 | if *userName == "" { 92 | flag.PrintDefaults() 93 | log.Fatal("-user_name or SLACK_USER_NAME required") 94 | } 95 | if *iconEmoji == "" { 96 | flag.PrintDefaults() 97 | log.Fatal("-icon_emoji or SLACK_ICON_EMOJI required") 98 | } 99 | 100 | attachment := Attachment{ 101 | MrkdwnIn: []string{"text", "pretext"}, 102 | AuthorIcon: *authorIcon, 103 | AuthorLink: *authorLink, 104 | AuthorName: *authorName, 105 | Color: *color, 106 | Fallback: *fallback, 107 | FooterIcon: *footerIcon, 108 | Footer: *footer, 109 | ImageURL: *imageURL, 110 | Pretext: *pretext, 111 | Text: *text, 112 | ThumbURL: *thumbURL, 113 | TitleLink: *titleLink, 114 | Title: *title, 115 | } 116 | 117 | fields := addField([]Field{}, *field1Title, *field1Value, *field1Short) 118 | fields = addField(fields, *field2Title, *field2Value, *field2Short) 119 | fields = addField(fields, *field3Title, *field3Value, *field3Short) 120 | fields = addField(fields, *field4Title, *field4Value, *field4Short) 121 | fields = addField(fields, *field5Title, *field5Value, *field5Short) 122 | fields = addField(fields, *field6Title, *field6Value, *field6Short) 123 | fields = addField(fields, *field7Title, *field7Value, *field7Short) 124 | fields = addField(fields, *field8Title, *field8Value, *field8Short) 125 | fields = addField(fields, *field9Title, *field9Value, *field9Short) 126 | fields = addField(fields, *field10Title, *field10Value, *field10Short) 127 | 128 | if len(fields) > 0 { 129 | attachment.Fields = fields 130 | } 131 | 132 | payload := Payload{ 133 | Attachments: []Attachment{attachment}, 134 | LinkNames: true, 135 | Mrkdwn: true, 136 | Username: *userName, 137 | IconEmoji: *iconEmoji, 138 | Channel: *channel, 139 | Thread: *thread, 140 | } 141 | 142 | notifier := NewSlackNotifier(*webhookURL) 143 | err := notifier.Notify(payload) 144 | if err != nil { 145 | fmt.Println("slack-notifier: Failed to sent message to Webhook URL. Error: ", err.Error()) 146 | } else { 147 | fmt.Println("slack-notifier: Sent message to Webhook URL") 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /slack_notifier.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | ) 9 | 10 | // SlackNotifier type 11 | // Set DryRun to 'true' to print the message to the console for testing (not send it to the Slack channel) 12 | type SlackNotifier struct { 13 | WebhookURL string 14 | DryRun bool 15 | } 16 | 17 | // Payload is a Slack message with attachments 18 | type Payload struct { 19 | Attachments []Attachment `json:"attachments"` 20 | LinkNames bool `json:"link_names"` 21 | Mrkdwn bool `json:"mrkdwn"` 22 | IconEmoji string `json:"icon_emoji"` 23 | Username string `json:"username"` 24 | Channel string `json:"channel"` 25 | Thread string `json:"thread_ts,omitempty"` 26 | } 27 | 28 | // Attachment for a Slack message 29 | // https://api.slack.com/docs/message-attachments 30 | type Attachment struct { 31 | AuthorIcon string `json:"author_icon"` 32 | AuthorLink string `json:"author_link"` 33 | AuthorName string `json:"author_name"` 34 | Color string `json:"color"` 35 | Fallback string `json:"fallback"` 36 | Fields []Field `json:"fields"` 37 | FooterIcon string `json:"footer_icon"` 38 | Footer string `json:"footer"` 39 | ImageURL string `json:"image_url"` 40 | MrkdwnIn []string `json:"mrkdwn_in"` 41 | Pretext string `json:"pretext"` 42 | Text string `json:"text"` 43 | ThumbURL string `json:"thumb_url"` 44 | TitleLink string `json:"title_link"` 45 | Title string `json:"title"` 46 | Ts int64 `json:"ts"` 47 | } 48 | 49 | // Field of an attachment 50 | type Field struct { 51 | Short bool `json:"short"` 52 | Title string `json:"title"` 53 | Value string `json:"value"` 54 | } 55 | 56 | // NewSlackNotifier creates a new SlackNotifier 57 | func NewSlackNotifier(webhookURL string) SlackNotifier { 58 | return SlackNotifier{ 59 | WebhookURL: webhookURL, 60 | } 61 | } 62 | 63 | // Notify sends a message to the Slack channel 64 | func (sn SlackNotifier) Notify(message Payload) error { 65 | data, err := json.Marshal(message) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | if sn.DryRun { 71 | fmt.Println(string(data)) 72 | return nil 73 | } 74 | 75 | body := bytes.NewBuffer(data) 76 | request, err := http.NewRequest("POST", sn.WebhookURL, body) 77 | if err != nil { 78 | return err 79 | } 80 | 81 | request.Header.Add("Content-Type", "application/json; charset=utf-8") 82 | 83 | client := &http.Client{} 84 | _, err = client.Do(request) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | return nil 90 | } 91 | --------------------------------------------------------------------------------