Slack messages from GitHub workflow

At GRRR we like to get a notification in Slack after a build, test, or deployment finishes. The notification contains the status (success/failure/cancelled) and which branch or tag it happened with. A simple message to inform team members about the state of the application.

To get this job done, we’ve used the action edge/simple-slack-notify, made by Adam K Dean. Unfortunately, this action has been discontinued and started to throw deprecation warnings (NodeJS 12 and ::set-output). So, what to do? Fork it? Ignore it? Stop sending messages?

No. I’ve decided to figure out a way to send the notification with the least amount of dependencies. And I’m happy to share it with you.

But before we can dive into the code, you have to create a Slack app first. Go to api.slack.com/apps/ and hit the “Create New App” button.

Screenshot of the page where you can create a Slack app.

After creating and installing the app in your workspace you have to enable incoming Webhooks. Go to the Incoming Webhooks feature, use the toggle in the top-right corner and follow the instructions.

Screenshot of the incoming webhooks page

Next, you have to create a webhook URL. Hit the “Add New Webhook to Workspace” button and select the channel you want to send a message to. For testing purposes, you can use your personal channel, as I do. And yes, you can send direct messages to yourself. Very handy for testing features like this.

This URL contains authentication information so handle it as a secret. Add it to the GitHub repository secrets with the name SLACK_WEBHOOK_URL.

Screenshot of the webhook urls for your workspace

Now let us get on with the code. I walk you through it step by step.

Start by adding a job to your workflow.

1send_notification_to_slack:
2    name: Send notification to Slack
3    runs-on: ubuntu-22.04

The job must always run, even if previous jobs have failed. Normally jobs won’t run or will be cancelled when others fail. The notification must be sent after one or more jobs, or else you always get a notification. In this example, the job must wait for job_a and job_b to finish, no matter its result.

4if: always()
5needs: [job_a, job_b]

Now we are going to add a step to the job. First, we have to define the different notifications.

 6steps:
 7    - run: |
 8          successText=":octocat: <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Build #${{ github.run_number }}> of *${{ github.repository }}@${{ github.ref_name }}* by *${{ github.actor }}* completed successfully."
 9          failureText=":octocat: <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Build #${{ github.run_number }}> of *${{ github.repository }}@${{ github.ref_name }}* by *${{ github.actor }}* failed."
10          cancelledText=":octocat: <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Build #${{ github.run_number }}> of *${{ github.repository }}@${{ github.ref_name }}* by *${{ github.actor }}* was cancelled.😥"          

You see a lot of ${{ ... }} in the code. This is GitHub Actions syntax, not shell script. It injects information into the command before it runs. A lot of information is available, read about the Context in the docs.

Then we have to know the final status of dependent jobs. To do so, we use some clever GitHub Action syntax. First, the function contains, which does exactly what its name says: check whether one of the items in the given list has the same value as the second parameter.

11       status="${{ (contains(needs.*.result, 'cancelled') && 'cancelled') || (contains(needs.*.result, 'failure') && 'failure') || 'success' }}"

We give it needs.*.result, this is a list with results from the needed jobs. So in this case it contains needs.job_a.result and needs.job_b.result. This remarkable feature makes this script easier to maintain because you only reference the job names in the jobs configuration.

Secondly, add the following if-statement to convert the found status into the right parameters for the API call.

13       if [ "$status" = 'success' ]; then
14        color='good'
15        text=$successText
16       elif [ "$status" = 'failure' ]; then
17        color='danger'
18        text=$failureText
19       elif [ "$status" = "cancelled" ]; then
20         color='warning'
21         text=$cancelledText
22       fi

Finally, we can add the HTTP POST request to the webhook URL. You can use much more advanced formatting to create more fancy messages.

24echo "{attachments: [{text: \"$text\", color: \"$color\"}]}" | curl \
25    "${{ secrets.SLACK_WEBHOOK_URL }}" \
26    -X "POST" \
27    --header "Content-Type: application/json" \
28    --data-binary @-

That’s it. Thanks for reading, and I’m leaving you with the complete code.

 1send_notification_to_slack:
 2    name: Send notification to Slack
 3    runs-on: ubuntu-22.04
 4    if: always()
 5    needs: [job_a, job_b]
 6    steps:
 7        - run: |
 8              successText=":octocat: <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Build #${{ github.run_number }}> of *${{ github.repository }}@${{ github.ref_name }}* by *${{ github.actor }}* completed successfully."
 9              failureText=":octocat: <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Build #${{ github.run_number }}> of *${{ github.repository }}@${{ github.ref_name }}* by *${{ github.actor }}* failed."
10              cancelledText=":octocat: <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Build #${{ github.run_number }}> of *${{ github.repository }}@${{ github.ref_name }}* by *${{ github.actor }}* was cancelled.😥"
11              status="${{ (contains(needs.*.result, 'cancelled') && 'cancelled') || (contains(needs.*.result, 'failure') && 'failure') || 'success' }}"
12
13              if [ "$status" = 'success' ]; then
14               color='good'
15               text=$successText
16              elif [ "$status" = 'failure' ]; then
17               color='danger'
18               text=$failureText
19              elif [ "$status" = "cancelled" ]; then
20                color='warning'
21                text=$cancelledText
22              fi
23
24              echo "{attachments: [{text: \"$text\", color: \"$color\"}]}" | curl \
25                "${{ secrets.SLACK_WEBHOOK_URL }}" \
26                -X "POST" \
27                --header "Content-Type: application/json" \
28                --data-binary @-