🔔

通知くらいまとめて管理しないか?Lambda+GitHubAPI+SlackAPIでSlackにGitHubとGmailの通知を集約する

2024/04/19に公開

はじめに

  • この記事の概要
    • GitHubの通知に対する迅速な反応の重要性について解説します。
    • GitHubとGmail(E-mail)をSlackに集約するおすすめの方法を紹介します。
  • この記事の対象読者
    • 通知周りの設定が疎かになっている方
    • GitHubを使用してチーム開発を行っている開発者やプロジェクトマネージャー。
    • GitHubの通知を受け取りたいが適切な方法がわからない方。
  • この記事で伝えたいこと
    • 通知に迅速に反応することは重要であると考えていること
    • チーム開発をするなら通知周りの設定はしてほしいこと

GitHub

GitHubの通知に反応する重要性

GitHubの通知に迅速に反応することは非常に重要です。特にチーム開発におけるレビュー通知は、反応が遅くなればなるほどプロジェクトの生産性が低下すると言っても過言ではありません。
もちろんレビューがpenndingの時でも開発が続けられるよう適切なタスク分割は非常に重要です。
しかし必ずしも適切なタスク分割ができるとは限りませんし、仮に完璧にタスクが管理されたプロジェクトだとしても、適切なレビューを迅速に行うことが無駄になることはないでしょう。

あなたのチームメイトはSlackでレビュー依頼をしていませんか?

ここを見てくれているあなたのチームメイトが、Slackでメンションをつけてレビュー依頼をしている場面を見たことがありますか?
仮にそのような依頼が慢性的に続いているようであれば非常に危険な状態であると言えます。チームとして早急に解決するべき課題になるでしょう。
そして、レビュー依頼のメンション先があなただったら...あなたのレビューはきっと遅く、メンションしたメンバーは、待っていてもあなたはレビューをしてくれないと思っていることでしょう。
もし、まだあなたがGitHubの通知を気にかけていないのであればGitHubの通知をSlackに集約することをお勧めします。

メールで通知されるから大丈夫と思っているあなたへ

確かにGitHubの通知はメールで通知することは可能です。
しかし、関わるプロジェクトが多くなるほど、watchするリポジトリが増えるほど、プロジェクトに関わる人が増えるほど、あなたに関わる重要な通知を見逃す確率が上がっていきます。
なぜなら、GitHubのメール通知はあなたに関係ない通知もメールとして送信してしまうからです。
もし、すでにあなたに関わる重要な通知を見逃しがちであるならば、メールで通知を確認することをやめることをお勧めします。

おすすめするGitHubの通知の受け取り方

Gitify


Gitifyは、GitHubの通知をデスクトップに直接表示することができます。
これにより、ブラウザから離れることなく作業に集中しやすくなります。さらに、カスタマイズ可能な通知設定や軽量なインターフェースなど、使いやすさにもこだわっています。
GitHubから通知を取得する方法としてhttps://github.com/notificationsをfetchしているため、適切な通知方法をGitHub上で行っていないと通知されません。

デメリットとして、スマホ対応していないことが挙げられると思います。

GitHubの公式アプリ


GitHubの公式アプリでは、リアルタイムでプッシュ通知を受け取ることができます。リポジトリへの新しいプッシュ、プルリクエストのレビュー依頼、コメントなど、重要なアクションが行われるたびにすぐに通知を受け取ることができます。
また、通知をカスタマイズして、重要なイベントにのみ集中することができます。たとえば、特定のリポジトリやプルリクエストに関する通知のみを受け取るように設定することができます。

GitHubの公式アプリのデメリットとしては、リマインドしてくれないことが挙げられると思います。自分は会議に参加している最中に通知が来て忘れてしまうということもあったりしました。

GitHubのSlackアプリ


GitHub Slackアプリを導入することで、GitHub上での重要なアクティビティに関するリアルタイムの通知をSlackチャンネルに受け取ることができます。プッシュされたコード、プルリクエストのレビュー、イシューの作成など、チーム全体が関心を持つアクションについてすぐに通知を受け取ることができます。
GitHubのメンションをSlackのメンションに変換してくれたりできるので、結構便利だったりします。

しかし、こちらの唯一のデメリットとして、organizationによっては許可されていないことが多く、特にOSSではほぼ許可されていないことが挙げられます。

自力で作っちゃう

これが一番おすすめです

GitHubAPIを定期的にポーリングして、SlackAPIで通知することで実現します。Gitifyと同様に/notificationsをポーリングするのでGitHubのSlackアプリのように許可されていないことが少ないですし、Slackに通知するのでスマホから確認可能。しかも定期実行で確認するまで永遠にリマインドし続けることも可能という優れもの。
またSlackに通知を集約できるので、後述するGmailの通知だったり、システムの障害通知、セキュリティアラート等々の通知もまとめて管理できて非常にスマートです。
デメリットとして実装に時間がかかる点が挙げられますが、コピペで動くようにコードを公開しますので、割とすぐ実装できちゃうと思います。

GitHubの通知をSlackに集約する

今回はAWS Lambdaを使用して実装していきます。自分は将来的にアレクサとも接続したかったのでLambdaを選んでますが、GitHub ActionsでもGoogle Cloud Functions、Azure FunctionsでもなんでもOKです。

STEP1 新しいSlackワークスペースを作成する

新しいSlackワークスペースを作成しましょう。既存のワークスペースでもいいですが混同するので、通知用ワークスペースを作ることをお勧めします。

また、追々使うので通知用の新しいチャンネルを4つ作成します。今回は説明しやすいように、000_github_notification_assigned、000_github_notification_mentioned、000_github_notification_review_requested、000_github_notification_team_mentionedの4つ作成します。

作り方がわからない方はドキュメントを見てください。
https://slack.com/intl/ja-jp/help/articles/206845317-Slack-ワークスペースを作成する

STEP2 新しいSlackAppを作成する


以下にアクセスして、「Create New App」から新しいSlackAppを作成してください。
https://api.slack.com/apps


ここの選択は「From Scratch」を選択。

次に、アプリネームを適当に入力して、「Pick a workspace to develop your app in」で先程作成したワークスペースを選択します。


作成できたら、「OAuth & Permissions」を探して遷移し、Scopesの欄でchat:write.publicchat:writeの権限をつけます。


その後、上にスクロールして「Install to Workspace」からワークスペースにアプリを追加しましょう。


ワークスペースにアプリを追加すると、画像の場所に「Bot User OAuth Token」が追加されるので、忘れないようにメモ帳とかにコピペしておきます。(あとで使います)

STEP3 GitHubでPAT(personal access token)を作成する

以下のリンクからPATを作成してください。
https://github.com/settings/tokens/new?description=Slackに通知するやつ!&scopes=read%3Auser%2Cnotifications%2Crepo

Expirationは30daysになっていると思いますが、ここはよしなに変更して有効期限を設定してください。権限等は編集しなくて大丈夫です。
Generate tokenでPATを作成します。

PATが作成されていると思いますので、こちらもメモ帳等にコピペしておきます。(あとで使います)

STEP4 Lambda関数を作成する

以下のリンクからLambda関数を新規作成します。AWSのアカウントがない方は個々で作成してください。
https://ap-northeast-1.console.aws.amazon.com/lambda/home?region=ap-northeast-1#/create/function


上記画像のように選択して、「関数の作成」をクリックしてください。

STEP5 Lambda関数にLayerを作成する

今回使用するコードではaxiosを使用しているので、axiosを使用できるようにLayerをおきます。

以下のコマンドをターミナルで打ってください。

$ mkdir axios
$ npm init
# 全てEnterで問題ないです。

$ cd axios
$ npm install axios
$ zip -r axios_layer.zip node_modules

上記コマンドを全て実行するとaxios_layer.zipができていると思いますので、以下のリンクからレイヤーを作成します。
https://ap-northeast-1.console.aws.amazon.com/lambda/home?region=ap-northeast-1#/create/layer

上記の画像のように選択して、先程作成したaxios_layer.zipをアップロードして作成してください。
ここで、LayerのARNをコピーしておきます。

STEP6 Lambda関数にLayerを設定する

先程STEP4で作成したLambda関数にSTEP5で作成したLayerを適用します。
先程STEP4で作成したLambda関数を選択して、レイヤーの追加をクリックします。

画像のようにARNを指定をクリックし、先程コピーしたARNをペーストしてLayerを設定します。

関数の概要で以下のようにLayer(1)になっていればOKです。

STEP7 Lambda関数の中身をコピペする


以下のコードをindex.mjsにコピペしてください。

import axios from 'axios';

const GITHUB_API_ENDPOINT = 'https://api.github.com/notifications';
const SLACK_TOKEN = process.env.SLACK_TOKEN;
const SEPARATOR_STRING = "\n\n-------------------------------------------------------\n\n"

const SLACK_CHANNELS = {
    assigned: process.env.SLACK_CHANNEL_ASSIGNED,
    mentioned: process.env.SLACK_CHANNEL_MENTIONED,
    teamMentioned: process.env.SLACK_CHANNEL_TEAM_MENTIONED,
    reviewRequested: process.env.SLACK_CHANNEL_REVIEW_REQUESTED
};

export async function handler() {
    try {
        const notifications = await fetchGitHubNotifications();
        const categorizedNotifications = categorizeNotifications(notifications);
        await sendSlackNotifications(categorizedNotifications);
    } catch (error) {
        console.error('エラーが発生しました:', error);
        throw error;
    }
}

/**
 * GitHubから通知を取得する。
 * @returns {Promise<Object[]>} GitHubの通知情報の配列
 */
async function fetchGitHubNotifications() {
    try {
        const response = await axios.get('https://api.github.com/notifications', {
            headers: {
                'Authorization': `token ${process.env.GITHUB_ACCESS_TOKEN}`,
                'Accept': 'application/vnd.github.v3+json'
            }
        });
        return response.data;
    } catch (error) {
        console.error('GitHubの通知を取得できませんでした:', error.response.status, error.response.statusText);
        throw error;
    }
}


/**
 * GitHubの通知をカテゴリに分類する。
 * @param {Object[]} notifications GitHubの通知情報の配列
 * @returns {Object} カテゴリ別に分類された通知情報のオブジェクト
 */
function categorizeNotifications(notifications) {
    return notifications.reduce((categories, notification) => {
        const category = getNotificationCategory(notification);
        if (category) {
            categories[category].push(notification);
        }
        return categories;
    }, {
        assigned: [],
        mentioned: [],
        teamMentioned: [],
        reviewRequested: []
    });
}


/**
 * 通知のカテゴリを取得する。
 * @param {Object} notification 通知情報
 * @returns {string|null} カテゴリ名
 */
function getNotificationCategory(notification) {
    switch (notification.reason) {
        case 'assign':
            return 'assigned';
        case 'mention':
            return 'mentioned';
        case 'team_mention':
            return 'teamMentioned';
        case 'review_requested':
            return 'reviewRequested';
        default:
            return null;
    }
}


async function sendSlackNotifications(categorizedNotifications) {
    const sendRequests = Object.entries(categorizedNotifications).map(async ([category, notifications]) => {
        if (notifications.length > 0) {
            const messages = await Promise.all(notifications.map(formatMessage));
            const channel = SLACK_CHANNELS[category];
            if (channel) {
                await sendSlackNotification(`@channel <https://github.com/notifications|${messages.length}件の通知を確認してください>` + SEPARATOR_STRING + messages.join(SEPARATOR_STRING), channel);
            } else {
                console.error(`Slackチャンネルが指定されていません: ${category}`);
            }
        }
    });
    await Promise.all(sendRequests);
}

/**
 * Slackに通知を送信する。
 * @param {string} message 送信するメッセージ
 * @param {string} channel 送信先のSlackチャンネル
 * @returns {Promise<void>}
 */

async function sendSlackNotification(message, channel) {
    try {
        const response = await axios.post('https://slack.com/api/chat.postMessage', {
            channel: channel,
            blocks: [
                {
                    "type": "section",
                    "text": {
                        "type": "mrkdwn",
                        "text": message
                    }
                }
            ]
        }, {
            headers: {
                'Authorization': `Bearer ${SLACK_TOKEN}`,
                'Content-Type': 'application/json'
            }
        });
        console.log('Slackに通知を送信しました:', response.data);
    } catch (error) {
        console.error('Slackに通知を送信できませんでした:', error.response.status, error.response.statusText);
        throw error;
    }
}

/**
 * GitHubの通知情報からメッセージをフォーマットする。
 * @param {Object} notification GitHubの通知情報
 * @returns {Promise<string>} フォーマットされたメッセージ
 */

async function formatMessage(notification) {
    try {
        console.log(notification)
        const response = await axios.get(notification.subject.url, {
            headers: {
                'Authorization': `token ${process.env.GITHUB_ACCESS_TOKEN}`,
                'Accept': 'application/vnd.github.v3+json'
            }
        });
        const subjectData = response.data;
        const prefix =  "`" + notification.subject.type + "` " + "`" + subjectData.state + "` " + "`" + subjectData.mergeable_state + "`";
        const line1 = prefix + `<${subjectData.html_url}|[${notification.repository.full_name}] *${notification.subject.title}*>\n`
        const line2 = "`" + "+ " + subjectData.additions + "`" + " `" + "- " + subjectData.deletions + "`  " + subjectData.commits + " commit(s)  " + subjectData.review_comments + " review_comment(s)\n";
        const line3 = `Author: <${subjectData.user.html_url}|${subjectData.user.login}>\n` + "reviewer(s): "+ subjectData.requested_reviewers.map((d) => `<${d.html_url}|${d.login}>`).join(" ") + "\n";
        const line4 = "created_at: " + subjectData.created_at + " updated_at: " + subjectData.updated_at + "\n"
        const line5 = subjectData.body || "";

        return line1 + line2 + line3 + line4 + line5;
    } catch (error) {
        console.error('GitHubの通知情報を取得できませんでした:', error.response.status, error.response.statusText);
        throw error;
    }
}

コピペが完了したら「Deploy」ボタンからデプロイしてください。

STEP8 Lambdaに環境変数を設定する


Lambda関数の詳細→設定→環境変数で、Lambdaに環境変数を追加できます。
追加する必要があるのは以下です。
GITHUB_ACCESS_TOKEN: STEP3で入手したPATをコピペします。
SLACK_TOKEN: STEP2で入手したトークンをコピペします。
SLACK_CHANNEL_ASSIGNED、SLACK_CHANNEL_MENTIONED、SLACK_CHANNEL_REVIEW_REQUESTED、SLACK_CHANNEL_TEAM_MENTIONEDは、STEP1で作成したチャンネル名を入力します。

入力が完了したら保存してください。

STEP9 Lambdaにトリガーを設定する


関数の概要からトリガーを追加をクリックします。


画像のようにCloud Watch Eventを選択し、あとは適当に入力します。Schedule expressionはcron(0,30 * * * ? *)を入力します。

以上9STEPで完了です。
00分と30分にGithubの通知が溜まっていれば@chanel付きで通知してくれます。通知頻度を変更したい場合は、STEP9のトリガーのcronを変更してください。

↑こんな感じに通知されます

かかる費用とか

30分に一回程度の実行であればほぼ無料でできると思っていいと思います。
まあ、導入後は定期的に請求額見て欲しいです。
なんなら、Slackに通知してもいいかも?

番外編OSSのリリース通知をSlackに集約する

ここを読んでいる方ならOSSのライブラリを使っていると思います。そうなってくると使っているライブラリの更新を追いたくなるのがエンジニアというものです。
watchすればいいと考える方もいらっしゃかもしれませんが、むやみやたらにwatchし続けるとタイムラインは崩壊してしまうのでおすすめできません。
そこで使うのはAtomフィードです。GitHubにはAtomフィードが存在し、それをSlackのRSSリーダーでよんであげることでチャンネルに流します。
urlはhttps://github.com/{author}/{repo}/releases.atomです。
例えばTypeScriptだったら以下になります。
https://github.com/microsoft/TypeScript/releases.atom

Gmail

仕事で多くの方がGmailを使用していると思います。Gmailを使用していなくても他のE-mailを使用していることでしょう。これらの通知もSlackに集約することをおすすめします。
Slackに課金しているワークスペースをお持ちであれば、Slackのメールアドレス宛に転送設定するだけでチャンネルに流れます。

詳しいやり方は割愛させていただきます。以下のDocsを見ればすぐできちゃうと思います。
https://slack.com/intl/ja-jp/help/articles/206819278-Slack-にメールを送信する#h_01F4WDZG8RTCTNAMR4KJ7D419V

まとめ

GitHubとGmailの通知をSlackに集約するススメをさせていただきました。
GitHubとGmailの通知以外にも、人によっては集約できるものがまだまだあるとおもいます。
(自分の場合は、自サイトのお問い合わせの一次受けや、自サイドとコストアラート、Asanaやjiraのチケット更新の通知など)
エンジニアはさまざまな通知を受け取りながら仕事をすると思います。この記事に出会ったのも何かの縁です。このタイミングでFaasを使って通知まわりをSlackに集約してみませんか?

Discussion