├── tests ├── fixture.json ├── fixture2.json ├── command.bats └── pipeline.bats ├── docker-compose.yml ├── hooks └── command ├── plugin.yml ├── .github └── workflows │ └── test.yml ├── LICENSE ├── lib └── pipeline.bash └── README.md /tests/fixture.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "email": "me@example.com", 4 | "slackId": "U1234" 5 | }, 6 | { 7 | "email": "other@example.com", 8 | "slackId": "U5678" 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /tests/fixture2.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "email": "me@example.com", 4 | "slackId": "U1234", 5 | "github": "dev1" 6 | }, 7 | { 8 | "email": "other@example.com", 9 | "slackId": "U5678", 10 | "github": "dev2" 11 | } 12 | ] 13 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | lint: 3 | image: buildkite/plugin-linter 4 | command: ["--id", "envato/build-failed-notify-slack"] 5 | volumes: 6 | - ".:/plugin:ro" 7 | 8 | tests: 9 | image: buildkite/plugin-tester 10 | volumes: 11 | - ".:/plugin:ro" 12 | -------------------------------------------------------------------------------- /hooks/command: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | DIR="$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)" 6 | 7 | # shellcheck source=lib/pipeline.bash 8 | source "$DIR/../lib/pipeline.bash" 9 | 10 | echo "~~~ Contents of notify pipeline" 11 | 12 | pipeline 13 | 14 | echo "+++ Uploading notify pipeline" 15 | 16 | pipeline | buildkite-agent pipeline upload -------------------------------------------------------------------------------- /plugin.yml: -------------------------------------------------------------------------------- 1 | name: Build Failed Notify Slack Buildkite Plugin 2 | description: "@'s the creator of the failed build via a mapping file" 3 | author: https://github.com/envato/build-failed-notify-slack-buildkite-plugi 4 | requirements: 5 | - jq 6 | configuration: 7 | properties: 8 | mapping_file: 9 | type: string 10 | channel: 11 | type: string 12 | required: 13 | - mapping_file 14 | - channel 15 | additionalProperties: false 16 | -------------------------------------------------------------------------------- /tests/command.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load '/usr/local/lib/bats/load.bash' 4 | 5 | # Uncomment the following line to debug stub failures 6 | # export BUILDKITE_AGENT_STUB_DEBUG=/dev/tty 7 | 8 | 9 | @test "Uploading the contents" { 10 | stub buildkite-agent "pipeline upload : echo done" 11 | 12 | run "$PWD/hooks/command" 13 | 14 | assert_success 15 | assert_output --partial "Contents of notify pipeline" 16 | assert_output --partial "Uploading notify pipeline" 17 | 18 | unstub buildkite-agent 19 | } -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: tests 3 | on: [push] 4 | jobs: 5 | plugin-tests: 6 | name: Tests 7 | runs-on: ubuntu-latest 8 | container: 9 | image: buildkite/plugin-tester:latest 10 | volumes: 11 | - "${{github.workspace}}:/plugin" 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: tests 15 | run: bats tests/ 16 | plugin-lint: 17 | name: Lint 18 | runs-on: ubuntu-latest 19 | container: 20 | image: buildkite/plugin-linter:latest 21 | volumes: 22 | - "${{github.workspace}}:/plugin" 23 | steps: 24 | - uses: actions/checkout@v2 25 | - name: lint 26 | run: lint --id envato/build-failed-notify-slack 27 | plugin-shellcheck: 28 | name: Shellcheck 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v2 32 | - name: Run ShellCheck 33 | uses: ludeeus/action-shellcheck@master 34 | with: 35 | check_together: "yes" 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Envato 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/pipeline.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | function pipeline() { 6 | mapping_file="${BUILDKITE_PLUGIN_BUILD_FAILED_NOTIFY_SLACK_MAPPING_FILE-}" 7 | 8 | if [[ $mapping_file == s3://* ]] ;then 9 | tmp_file=$(mktemp) 10 | aws s3 cp "$mapping_file" "$tmp_file" &> /dev/null 11 | mapping_file="$tmp_file" 12 | fi 13 | 14 | channel="${BUILDKITE_PLUGIN_BUILD_FAILED_NOTIFY_SLACK_CHANNEL-}" 15 | email="${BUILDKITE_BUILD_CREATOR_EMAIL:-unknown}" 16 | creator="${BUILDKITE_BUILD_CREATOR:-unknown}" 17 | branch="${BUILDKITE_BRANCH:-main}" 18 | 19 | if [[ -f "$mapping_file" ]]; then 20 | # Lookup the email address first, fallback to looking up github username 21 | slackId=$(jq -r ".[] | select(.email==\"$email\" or .github==\"$creator\").slackId" < "$mapping_file") 22 | fi 23 | 24 | if [[ -n "${slackId-}" ]]; then 25 | creator="<@${slackId-}>" 26 | fi 27 | 28 | echo "steps: []" 29 | echo 30 | echo "notify:" 31 | echo " - slack:" 32 | echo " channels:" 33 | echo " - \"$channel\"" 34 | echo " message: \"The most recent \`$branch\` branch build by $creator has failed, please take a look.\"" 35 | echo " if: build.state == \"failed\"" 36 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Build Failed Notify Slack Buildkite Plugin 2 | 3 | Buildkite has a notifications feature that can notify channels in slack about build status. The problem is that it's missing a mapping to the build creator's slack ID. 4 | 5 | This plugin uses the [notifications feature](https://buildkite.com/docs/pipelines/notifications) and a mapping file to @at the creator of a failed build. 6 | 7 | ## Example 8 | 9 | Add the following to your `pipeline.yml`: 10 | 11 | ```yml 12 | steps: 13 | - label: ":slack: Notify on fail" 14 | if: build.branch == "main" 15 | plugins: 16 | - envato/build-failed-notify-slack#v1.1.0: 17 | mapping_file: slack_users.json 18 | channel: "#my-channel" 19 | ``` 20 | 21 | Mapping files can be downloaded from s3 22 | 23 | ```yml 24 | steps: 25 | - label: ":slack: Notify on fail" 26 | if: build.branch == "main" 27 | plugins: 28 | - cultureamp/aws-assume-role#v0.2.0: 29 | role: "arn:aws:iam::123456789012:role/example-role" 30 | - envato/build-failed-notify-slack#v1.1.0: 31 | mapping_file: s3://my-bucket/slack_users.json 32 | channel: "#my-channel" 33 | ``` 34 | 35 | ## Configuration 36 | 37 | ### `mapping_file` (Required, string) 38 | 39 | JSON file with an array of users slack id's and email addresses (email used in Buildkite, appears as `BUILDKITE_BUILD_CREATOR_EMAIL`) 40 | 41 | eg. 42 | 43 | ```json 44 | [ 45 | { 46 | "email": "me@example.com", 47 | "slackId": "U1234" 48 | }, 49 | { 50 | "email": "other@example.com", 51 | "slackId": "U5678" 52 | } 53 | ] 54 | ``` 55 | 56 | ### `channel` (Required, string) 57 | 58 | Including the hash, a channel in slack. 59 | 60 | > [!NOTE] 61 | > The slack channel must be configured to receive notifications with the [Buildkite Builds](https://slack.com/marketplace/AN19RS48G) Slack app. More support for integrating with Buildkite can be found in their [documentation](https://buildkite.com/docs/pipelines/integrations/other/slack) 62 | 63 | ## Developing 64 | 65 | To run the tests: 66 | 67 | ```shell 68 | docker-compose run --rm tests 69 | ``` 70 | 71 | To run the linting: 72 | 73 | ```shell 74 | docker-compose run --rm lint 75 | ``` 76 | -------------------------------------------------------------------------------- /tests/pipeline.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load '/usr/local/lib/bats/load.bash' 4 | load '../lib/pipeline' 5 | 6 | # Uncomment the following line to debug stub failures 7 | # export BUILDKITE_AGENT_STUB_DEBUG=/dev/tty 8 | 9 | @test "looks up the slackId based on email in a json file" { 10 | export BUILDKITE_PLUGIN_BUILD_FAILED_NOTIFY_SLACK_CHANNEL="#my-channel" 11 | export BUILDKITE_PLUGIN_BUILD_FAILED_NOTIFY_SLACK_MAPPING_FILE=tests/fixture.json 12 | export BUILDKITE_BUILD_CREATOR_EMAIL=me@example.com 13 | export BUILDKITE_BUILD_CREATOR=me 14 | export BUILDKITE_BRANCH=main 15 | 16 | run pipeline 17 | 18 | assert_success 19 | assert_output << EOM 20 | steps: [] 21 | 22 | notify: 23 | - slack: 24 | channels: 25 | - "#my-channel" 26 | message: "The most recent \`main\` branch build by <@U1234> has failed, please take a look." 27 | if: build.state == "failed" 28 | EOM 29 | } 30 | 31 | @test "looks up the slackId based on email in a json file on s3" { 32 | export BUILDKITE_PLUGIN_BUILD_FAILED_NOTIFY_SLACK_CHANNEL="#my-channel" 33 | export BUILDKITE_PLUGIN_BUILD_FAILED_NOTIFY_SLACK_MAPPING_FILE=s3://source/fixture.json 34 | export BUILDKITE_BUILD_CREATOR_EMAIL=me@example.com 35 | export BUILDKITE_BUILD_CREATOR=me 36 | export BUILDKITE_BRANCH=main 37 | 38 | stub mktemp ": echo tests/fixture.json" 39 | stub aws "s3 cp s3://source/fixture.json tests/fixture.json : echo done" 40 | 41 | run pipeline 42 | 43 | assert_success 44 | assert_output << EOM 45 | steps: [] 46 | 47 | notify: 48 | - slack: 49 | channels: 50 | - "#my-channel" 51 | message: "The most recent \`main\` branch build by <@U1234> has failed, please take a look." 52 | if: build.state == "failed" 53 | EOM 54 | 55 | stub aws 56 | } 57 | 58 | @test "looks up the slackId based on github username in a json file" { 59 | export BUILDKITE_PLUGIN_BUILD_FAILED_NOTIFY_SLACK_CHANNEL="#my-channel" 60 | export BUILDKITE_PLUGIN_BUILD_FAILED_NOTIFY_SLACK_MAPPING_FILE=tests/fixture2.json 61 | export BUILDKITE_BUILD_CREATOR_EMAIL=dev1@users.noreply.github.com 62 | export BUILDKITE_BUILD_CREATOR=dev1 63 | export BUILDKITE_BRANCH=main 64 | 65 | run pipeline 66 | 67 | assert_success 68 | assert_output << EOM 69 | steps: [] 70 | 71 | notify: 72 | - slack: 73 | channels: 74 | - "#my-channel" 75 | message: "The most recent \`main\` branch build by <@U1234> has failed, please take a look." 76 | if: build.state == "failed" 77 | EOM 78 | } 79 | 80 | @test "can gracefully fail the lookup" { 81 | export BUILDKITE_PLUGIN_BUILD_FAILED_NOTIFY_SLACK_CHANNEL="#my-channel" 82 | export BUILDKITE_PLUGIN_BUILD_FAILED_NOTIFY_SLACK_MAPPING_FILE=tests/fixture.json 83 | export BUILDKITE_BUILD_CREATOR_EMAIL=not-exist@example.com 84 | export BUILDKITE_BUILD_CREATOR="Some One" 85 | export BUILDKITE_BRANCH=main 86 | 87 | run pipeline 88 | 89 | assert_success 90 | assert_output << EOM 91 | steps: [] 92 | 93 | notify: 94 | - slack: 95 | channels: 96 | - "#my-channel" 97 | message: "The most recent \`main\` branch build by Some One has failed, please take a look." 98 | if: build.state == "failed" 99 | EOM 100 | } 101 | 102 | @test "can gracefully fail when file missing" { 103 | export BUILDKITE_PLUGIN_BUILD_FAILED_NOTIFY_SLACK_CHANNEL="#my-channel" 104 | export BUILDKITE_PLUGIN_BUILD_FAILED_NOTIFY_SLACK_MAPPING_FILE=not-real.json 105 | export BUILDKITE_BUILD_CREATOR_EMAIL=not-exist@example.com 106 | export BUILDKITE_BUILD_CREATOR="Some One" 107 | export BUILDKITE_BRANCH=main 108 | 109 | run pipeline 110 | 111 | assert_success 112 | assert_output << EOM 113 | steps: [] 114 | 115 | notify: 116 | - slack: 117 | channels: 118 | - "#my-channel" 119 | message: "The most recent \`main\` branch build by Some One has failed, please take a look." 120 | if: build.state == "failed" 121 | EOM 122 | } --------------------------------------------------------------------------------