Building a Unified Notification System with GitHub Actions

Building a Unified Notification System with GitHub Actions

8/6/2023, Published in Medium, DevTo and HashNode

1 min read

200

Explore the power of GitHub actions actions by integrating with multiple notification services including Slack, Chime, Teams, AWS SNS and more..

Introduction

In this blog, we will explore the process of building a GitHub actions for notifications services to publish actions workflow execution results for Slack, Discord, Teams, Chime, SNS. Also, I will share the steps to publish this to Github Actions market place.

You can find this action available in Github Actions Market place

Uninotify Actions Marketplace: https://github.com/marketplace/actions/uninotify

GitHub Repo : https://github.com/OSCloudysky/UniNotify

The Magic of GitHub Actions

GitHub Actions is an API for cause and effect on GitHub. You can orchestrate any workflow, based on any event, while GitHub manages the execution, provides rich feedback, and secures every step along the way.

The Power of Unified Notifications

The idea behind this implementation is to ability to use an action in any actions workflow to publish execution details to several notification services as shown in the diagram. Notification plays a big role in the CI/CD process and this allows teams to focus on building their workflow efficiently and not worry about integrations to their notification services.

Diving into the Implementation

The first step was to set up GitHub Actions repo and I have used the following template to bootstrap my github actions. This is highly recommended if you are planning to build custom actions using Typescript.

Actions Typescript github template: https://github.com/actions/typescript-action

Please find the repo link for my custom action to publish messages to multiple notification services. I have created a custom action in TypeScript that utilizes Octokit, a GitHub REST API client, to fetch details about the workflow, commit, and repository etc. UniNotify action is responsible to format the message including details such as workflow name, event name, run URL, commit messages etc. I have included support to send default messages and as part of next release, you will have the ability to send custom messages as well.

UniNotify repo : https://github.com/OSCloudysky/UniNotify

Now, let's dive into how this works and here is the entry point for the Action in main.ts file. As you can see here, it leverages Octokit, GitHub REST API Client. to fetch details about workflow, commit, and repo etc.

Then, it basically calls respective services which I have created as each component for better code management and reusable code.

import * as core from '@actions/core'
import * as github from '@actions/github'
import * as service from './services'
import {Octokit} from '@octokit/rest'
import fetch from 'node-fetch'

async function run(): Promise<void> {
  const messageType = core.getInput('messageType')
  const githubToken = core.getInput('githubToken')

  // Get more details information of the job
  const context = github.context
  const octokit = new Octokit({auth: `token ${githubToken}`, request: {fetch}})

  const commit = await octokit.repos.getCommit({
    owner: context.repo.owner,
    repo: context.repo.repo,
    ref: context.sha
  })

  const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`

  const message = `
    Workflow Name: ${context.workflow}
    Event: ${context.eventName}
    Run URL: ${runUrl}
    Triggered By: ${context.actor}
    Commit URL: https://github.com/${context.repo.owner}/${context.repo.repo}/commit/${context.sha}
    Commit Message: ${commit.data.commit.message}
  `

  try {
    const token = core.getInput('slackToken')
    if (messageType === 'slack') {
      await service.sendSlackMessage(token, context, commit)
    } else if (messageType === 'discord') {
      const webhookUrl = core.getInput('discordWebhookUrl')
      await service.sendDiscordMessage(webhookUrl, context, commit)
    } else if (messageType === 'chime') {
      const webhookUrl = core.getInput('chimeWebhookUrl')
      await service.sendChimeMessage(webhookUrl, context, commit)
    } else if (messageType === 'teams') {
      const webhookUrl = core.getInput('teamsWebhookUrl')
      await service.sendTeamsMessage(webhookUrl, message)
    } else if (messageType === 'sns') {
      core.info('Sending SNS message')
      const snsParams = {
        awsAccessKeyId: core.getInput('awsAccessKeyId'),
        awsSecretAccessKey: core.getInput('awsSecretAccessKey'),
        awsRegion: core.getInput('awsRegion'),
        snsTopicArn: core.getInput('snsTopicArn')
      }

      if (
        snsParams.awsAccessKeyId &&
        snsParams.awsSecretAccessKey &&
        snsParams.awsRegion &&
        snsParams.snsTopicArn
      ) {
        await service.sendSNSMessage(context, commit, snsParams)
      }
    } else {
      core.setFailed(
        'Unsupported messageType. Supported types are "slack" and "discord"'
      )
    }
  } catch (error) {
    if (error instanceof Error) {
      core.setFailed(error.message)
    } else {
      core.setFailed(`Unexpected error: ${JSON.stringify(error)}`)
    }
  }
}

run()

Let's look at one of the service and we will pick Slack for reference. Slack integration is done by token and you will find few more services that work with Webhook Urls to publish messages.

Here in slack service, I am basically defining the type for Context and Commit data that I am using. Slack offers a nice library to publish messages and i have used embedded content to set a style for the message.

Prior to this, follow the Slack documentation on how to create new slack app and create bot user with token here

import {WebClient} from '@slack/web-api'
type Context = {
  workflow: string
  eventName: string
  runId: number
  actor: string
  sha: string
  repo: {
    owner: string
    repo: string
  }
}

type Commit = {
  data: {
    commit: {
      message: string
    }
  }
}

export async function sendSlackMessage(
  token: string,
  context: Context,
  commit: Commit
): Promise<void> {
  // Styling the message
  const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`
  const message = {
    text: 'GitHub Actions Workflow Execution Details',
    blocks: [
      {
        type: 'divider' // divider at the top
      },
      {
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: `*Workflow Name:* ${context.workflow}`
        }
      },
      {
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: `*Event:* ${context.eventName}`
        }
      },
      {
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: `*Run URL:* <${runUrl}>`
        }
      },
      {
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: `*Triggered By:* ${context.actor}`
        }
      },
      {
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: `*Commit URL:* <https://github.com/${context.repo.owner}/${context.repo.repo}/commit/${context.sha}>`
        }
      },
      {
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: `*Commit Message:* ${commit.data.commit.message}`
        }
      },
      {
        type: 'divider' // divider at the top
      }
    ]
  }

  const web = new WebClient(token)
  await web.chat.postMessage({
    channel: '#general',
    text: message.text,
    blocks: message.blocks
  })
}

You will also need to define the input fields in action.yml file and here is a code snippet for the entire yml to support all the notification services listed above.

name: 'Uninotify'
description: 'Send notification to all messaging platforms'
author: 'Sandeep Yaramchitti'
inputs:
  githubToken:
    description: 'Github API token'
    required: true
  slackToken:
    description: 'Slack API token'
    required: false
  messageType:
    description: 'Type of message to send (slack, teams, chime)'
    required: true
  message:
    description: 'Message to send'
    required: false
  discordWebhookUrl:
    description: 'Webhook URL for discord'
    required: false
  chimeWebhookUrl:
    description: 'Webhook URL for chime'
    required: false
  teamsWebhookUrl:
    description: 'Webhook URL for teams'
    required: false
  awsAccessKeyId:
    description: 'AWS Access Key ID'
    required: false
  awsSecretAccessKey:
    description: 'AWS Secret Access Key'
    required: false
  awsRegion:
    description: 'AWS Region'
    required: false
  snsTopicArn:
    description: 'AWS SNS Topic ARN'
    required: false
runs:
  using: 'node16'
  main: 'dist/index.js'

Now, time to create a workflow to test this integration. Here is a workflow example on how to use UniNotify Action for slack integration.

Check out the Uninotify actions documentation for more information on required parameters.

Uninotify Github Actions: https://github.com/marketplace/actions/uninotify

name: 'build-test'
on: # rebuild any PRs and main branch changes
  pull_request:
  push:
    branches:
      - main
      - 'releases/*'
  workflow_dispatch:
  
jobs:
  notify:
    runs-on: ubuntu-latest
    steps:
    - name: Send Notification to slack
      uses: SandeepKumarYaramchitti/UniNotify@v1
      with:
        githubToken: ${{ secrets.GITHUB_TOKEN }}
        messageType: 'slack' 
        slackToken: ${{ secrets.SLACK_API_TOKEN }}

Once you run the above workflow, you should see the following slack message in the channel you directed the messages to be sent.

Challenges

The biggest challenge was handling different message formatting requirements for each platform. I had to read through each platform's API documentation to understand how to format messages effectively.

Another challenge was managing tokens securely. I decided to use GitHub's secret management capabilities to securely store our tokens.

Please do check out the action and let me in case. you have any feedback.

ArchitectureAutomationDevOps

ALL SYSTEMS ONLINE