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.
Sandeep Yaramchitti
- Bringing my ideas into life through Code.
ALL SYSTEMS ONLINE