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.

We’ve used the action edge/simple-slack-notify to get this job done. Thanks, Adam K Dean. But that action is discontinued and starts to throw deprecation warnings (NodeJS 12 and ::set-output). What to do, fork it? Ignore it? Stop sending messages? No. I 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.

Before we can dive into the code a Slack app must be created. 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, I use my personal channel. 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.

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

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

You see a lot of ${{ ... }} in the code. That’s 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.

 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.😥"       

Now 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.

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.

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

Then an 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

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

24       curl "${{ secrets.SLACK_WEBHOOK_URL }}" -X "POST" --header "Content-Type: application/json" \
25         --data '{attachments: [{text: \"$text\", color: \"$color\"}]}'

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       curl "${{ secrets.SLACK_WEBHOOK_URL }}" -X "POST" --header "Content-Type: application/json" \
25         --data '{attachments: [{text: \"$text\", color: \"$color\"}]}'