RustでGitHubの通知をSlackに流すアプリを作りました 

![banner-01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3827828/46611b8e-a401-4961-de9d-880d25fecae4.png)


※本記事は [株式会社ポーラ・オルビスホールディングス Advent Calendar 2024](https://qiita.com/advent-calendar/2024/pohd) の21日目の記事です。ITプロダクト開発チーム以外の方々からも多くの記事が投稿されていますので、ぜひご覧いただけますと幸いです。 


- - - 
はじめまして、 ポーラ・オルビスホールディングスのITプロダクト開発チームでエンジニアをしている大野と申します。 


自分の所属している ITプロダクト開発チームでは、実際に社内のエンジニアが手を動かしてアプリケーションを開発しており、ソースコードの管理にGitHubを使用しています。 
また、当チームはレビュー文化があり、誰かが書いたコードは任意のメンバーがレビューをし、属人化の防止やコードの改善に努めています。 



さて、レビューとなるとよく課題に上がるのはレビューの通知やレビューコメントの通知問題ですね。 

以前までは「レビューお願いします！！コメント返しました！！レビュー修正しました！！確認をお願いします！！OKです！！ありがとうございます！！マージしました！！」みたいなのをテキストで打ったり、デイリーのミーティングでリマインドしたりしていましたが、 
まず文字列を打つのが面倒ですし、かといって送り忘れるとレビューのリードタイムが長くなってしまい開発の効率が落ちるという問題があります。 

そんな課題を解決すべく、弊社のコミュニケーションツールとして使っているSlackにGitHubの通知を飛ばすAWS Lambdaを作成しました。 
まあ公式が連携用アプリケーションを用意してくれているのでそれで事足りるんですが、せっかくなので勉強がてら自作してみました。レッツ車輪の再発明。 
- - -
一応自作するメリットとしては公式で対応していない範囲まで自由にカスタマイズできる点が挙げられます。 
- 例えばコメント内のメンションをSlack上でのメンションに置き換えるとか、（作ってないけど…） 
- 特定ブランチにマージしたときだけ全体に通知するとか、（作ってないけど…） 
- 特定の人が残したコメントだけ他の人も参考にできるようSlackに残しておくとか。（作ってないけど…） 

いつか~~やる気がでたら~~需要が高まったら実装しようと思います。いつかね。 

# 実際に作ったもの
![Frame 1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3827828/5a4b932c-594f-291c-f4bf-55e6a984457e.png)

こんな感じです。 
通知のタイミングは 
- プルリクにapproveがついたとき、プルリク作成者に通知 
- プルリクにコメントがついたとき、プルリク作成者に通知 
- プルリクからreview requestが発行されたとき、requestを受けた人に通知 

の3種です。 
ちなみにアイコンはITプロダクト開発チームのキャラクターはじめちゃん[^1]です。 かわいい。 

[^1]:このキャラクターはcopilotを用いて作成しています。また、既存キャラクターに似せないルールで作成しました。 


# 大まかな流れ 
今回のアプリの大枠はこんな感じです。 

![Frame 3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3827828/2ca992f0-9a48-5cce-0a91-2436633303ed.png)

GitHubに何かアクションがあった際にLambdaにPOSTリクエストが飛ぶようにwebhookを設定します。 
Lambdaの関数はそのリクエストを受け取り、SlackのURLにメッセージをPOSTリクエストするとSlackにコメントが行きます。 

# ワークフロー作成
まずはSlackに送信するためのワークフローを作成します。 
ワークフローの起点はwebhook、引数には 
- Mention: ユーザID（メンション用） 
- Channel: チャンネルID（送信先チャンネル用） 
- Text: テキスト（本文用） 

を用意します。

<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3827828/b1374ddb-ca2c-6f26-6cd3-d1136041cc83.png">


リクエストを受け取ったらチャンネルに対してメンションとコメントを送信するよう設定しておきます。 
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3827828/45c9c812-eef2-2257-0c51-3dd19386c4d9.png">

# GitHubのリクエスト内容 
GitHubから送られてくるobjectは結構量が多いので割愛しますが、 
ヘッダのX-GitHub-Eventとボディのactionあたりでだいたいの処理を分岐できるかと思います。 

今回通知したい対象の X-GitHub-Eventとactionはこんな感じでした。 
<dl>
  <dt>レビュー依頼</dt>
  <dd>X-GitHub-Event: pull_requlest</dd>
  <dd>Action: review_requested</dd>
  <dt>レビューのコメント</dt>
  <dd>X-GitHub-Event: pull_requlest_review</dd>
  <dd>Action: submitted</dd>
</dl> 

他のパラメータは公式ドキュメントにいろいろ載っています。 

https://docs.GitHub.com/ja/webhooks/webhook-events-and-payloads 

コメントされた内容とかも送られてくるので、コメント内にNITSとか書いてあったら通知しないとか、MUSTとか書いてあったら全員に共有するとか、そんな感じの詳しい条件もいろいろできそうですね。 

# Lambdaの処理
今回は初めて触るRustで書いてみました。 

## ライブラリ 
Axum: 0.7.4 
Tokio: 1.36.0 
Serde: 1.0 
Serde_json: 1.0 
Reqwest: 0.12.7 

## 処理内容 
とりあえずサーバを立ち上げてリクエストを受けられるようにします。 
```rust:Main.rs
mod controller; 
mod util; 
use axum::{routing::post, Router}; 

#[tokio::main] 
async fn main() { 
    let app = Router::new().route("/", post(controller::git::root_handler));  
    let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap(); 
    axum::serve(listener, app).await.unwrap(); 
} 
```

Slackに送信する関数も作っておきます。 
```rust:Util/send_slack.rs
use std::collections::HashMap; 

pub async fn send_slack(text: &str, user: &str)-> Result<(), Box<dyn std::error::Error>> { 
let url = "slackのwebhookのURL"; 
   let mut map: HashMap<&str, &str> = HashMap::new(); 
   map.insert("mention", user); 
   map.insert("text", text); 
   map.insert("channel", "チャンネルID"); 
   let client = reqwest::Client::new(); 
   let result = client.post(url) 
       .json(&map) 
       .send() 
       .await?; 
   Ok(()) 
} 
```

メインの処理内容自体は結構シンプルです。 

POSTを受けたらヘッダのX-GitHub-Eventを見て、reviewなのかpull_requestなのかを確認して、それ次第で後続の処理を実行します。 

```rust:Git.rs
pub async fn root_handler(headers:HeaderMap, body: Json<Git>) -> impl IntoResponse { 
    let members = get_member(); 
    let event: &str = headers.get("X-GitHub-Event").unwrap().to_str().unwrap(); 
    if event == "pull_request" { 
      pull_request_handler(body, members).await; 
    } 
    else if event == "pull_request_review" { 
      review_handler(body, members).await; 
    } 
    return StatusCode::OK 
} 
```

 
### pull_request
プルリクの場合はbodyのaction名を見てreview_requestedだったらSlackに送信します。 
Bodyのrequested_reviewerのloginの中にIDが格納されているので、 
各メンバーのGitHub IDとSlack IDを宣言したオブジェクトを用意しておき、それを使ってGitHub IDからSlack IDに変換しています。 
```rust:Pull_request.rs
pub async fn pull_request_handler(body: Json<Git>, members: Vec<Member>) { 
    if body.action == "review_requested" { 
        let name =  match &body.requested_reviewer { 
            Some(reviewer) => { 
                &reviewer.login 
            }, 
            None => { 
                &"".to_string() 
            } 
        }; 
        let mention = members.into_iter().find(|member| member.git_id == *name).unwrap().slack_id; 
        let _ = send_slack(&format!("{:?} がプルリク見てって言ってるよ 見てあげなよ\n{:?}", body.pull_request.as_ref().unwrap().user.login, body.pull_request.as_ref().unwrap().html_url), &mention).await;         
    } 
} 
```

```rust
pub struct Member { 
    pub git_id: String, 
    pub slack_id: String 
} 
```
### pull_request_review
Pull_request_reviewはこんな感じ。 
こちらはreviewのstateにapprovedなのかそうでないのかが格納されているので、そこを参照して送る情報を出し分けてます。 
```rust:Review.rs
pub async fn review_handler(body: Json<Git>, members: Vec<Member>) {
    if body.action == "submitted" {
        let name: &String = &body.pull_request.as_ref().unwrap().user.login;
        let mention = members.into_iter().find(|member| member.git_id == *name).unwrap().slack_id;

        if body.review.as_ref().unwrap().state == "approved" {
            let _ = send_slack(&format!("{:?} がマージしていいよって言ってるよ よかったね\n{:?}",body.sender.as_ref().unwrap().login, body.pull_request.as_ref().unwrap().html_url), &mention).await;        
            return;
        }
        let _ = send_slack(&format!("{:?} がコメントしてくれたよ さっさとなおしな！\n{:?}", body.sender.as_ref().unwrap().login, body.pull_request.as_ref().unwrap().html_url), &mention).await;
    }
}
```

## デプロイ 
適当にimageにして適当にECRに上げます。 
Lambdaで動作させるためにRustのLambda用のadapterを入れてます。 
```Dockerfile
FROM rust:1.81.0-bookworm as builder 
WORKDIR /app 
COPY Cargo.toml Cargo.lock /app/ 
RUN mkdir src 
COPY src /app/src 
RUN cargo build --release 

FROM gcr.io/distroless/cc-debian12 
COPY --from=builder /app/target/release/git_monitoring /app/ 
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.8.4 /lambda-adapter /opt/extensions/lambda-adapter 
CMD ["/app/git_monitoring"] 
```
あとはECRからLambdaにデプロイして関数URL発行します。ほんと便利。 

# GitHubに設定 
Settings -> webhooks -> add webhookからwebhookを設定します。 
 ![Frame 2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3827828/644da404-7946-cf79-b281-1ee2ce93a32e.png)

Which events would you like to trigger this webhook? > Let me select individual events. 
で送信してほしいトリガを設定します。
今回はPull requestとPull request reviewsのみで十分でした。

<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3827828/6253cc3e-e687-82f1-40e7-2822aa093c49.png">


これでSlackに通知が飛ぶようになります！ 

# まとめ 
RustでGitHubの通知をSlackに送るアプリを作ってみました！ 
実際調べてみるとGitHubから送られてくるオブジェクトは結構いろいろなパラメータがあるので、もっといろいろできそうだなと思っています。作ってないけど。 
今後ほしい機能が増えていき次第どんどんアップデートしていく予定です。これこそがアジャイル。 

初Rustだったのでいろいろと躓きました…メモリ管理ちゃんとしてる言語は慣れてないので大変でした…TypeScriptとかめちゃ書きやすいんだなと… 
あと動かすをメインで書いてしまい理解しきれてないポイントが多いのでまた時間取って勉強します…精進します… 

まあでもすごい楽しかったので、今後もこんな感じで触ったことない言語の触ったことないフレームワークでまあまあ不要なものを作っていきたいと思います。