0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

RustAdvent Calendar 2024

Day 16

Rust + Cloudflare WorkesとGithubを連携して、Slackに通知する

Last updated at Posted at 2024-12-15

はじめに

この記事はhttps://qiita.com/advent-calendar/2024/rust の16日目の記事です。

自己紹介

Rust歴、約10ヶ月の中級Rust使いになりたいものです。仕事では、PHPやPythonをメインを扱っており、個人で日々、Rustを書いてます。Rustについては、まだまだ勉強中です。

Rust歴も浅く、皆さんのためになるようなRust情報を提供ができてないかもしれませんが、Rustが好きなので、アドベントカレンダーを通して何かしらできたらな、という思いです。
至らない点があると思いますが、温かい目で見守ってもらえば思います。

本題

勉強の一環として、Cloudflare Workes、Githubを連携して、issueやPullRequestをSlackに通知する仕組みを作ってみました。こちらを共有したいと思います。

背景

Rustでオリジナルアプリ作っていくうちに、ネタが尽きてきたので何かと連携したものでも作ってみようか、と思ったのがきっかけでした。

調べてみると、Cloudflare Workesのチュートリアルにちょうど作成したissueを通知するSlackbotがあったので、これを参考にRustで作ることにしました。

issue編

チュートリアルはnode.jsで作る手順になっているのを、Rustで実装しました。

Slackでslash(/issue)コマンド実行

コードはroutingの処理部分になります。
Slackに表示されるまでの流れは下記で、Cloudflare Workesでリクエストとレスポンスの処理を実施してます。

  1. Slackでの/issue owner/repository#issue番号 コマンドを実行
  2. 受け取ったリクエスト情報(owner/repository#issue番号)を元に、Gihubへissue情報取得リクエスト
  3. issue情報を元にSlack投稿用のjson作成
  4. 作成したjsonをレスポンス
  5. issueの情報をが表示される
pub async fn lookup(mut req: Request, _ctx: RouteContext<()>) -> Result<Response> {
    let body = req.text().await?;

    if body.is_empty() {
        return Response::error("Not found", 404);
    }

    let params: SlashCommandRequest = serde_qs::from_str(&body).unwrap();

    let issue = Github
        .fetch_issue(&params.text)
        .await
        .map_err(|e| worker::Error::RustError(format!("{}", e)));

    match issue {
        Ok(issue) => {
            let result = serde_json::json!(
                SlashCommandResponse {
                    blocks: Slack.construct_gh_issue_slack_message(
                        &issue,
                        &Slack.issue_slack_message_text_lines(&issue, &body, "")
                    ),
                    response_type: "in_channel".to_string(),
                }
            );

            Response::from_json(&result)
        },
        Err(e) => Response::error(format!("{:?}", e), 500),
    }
}

Slack表示

20240710213903.png

issue作成自動通知〜Slack表示

Githubにwebhookを設定し、issue作成のアクションがあると、Slackに通知されるまでの流れが下記です。
こちらはGithubからリクエストされたものをSlack投稿用jsonへ変換し、通知してます。

  1. webhookからのリクエストをjsonで取得
  2. jsonを元にslack投稿用のjson作成
  3. Slackへjsonをリクエスト
  4. Slackに作成したissueを表示
pub async fn webhook(mut req: Request, ctx: RouteContext<()>) -> Result<Response> {
    let body: GithubWebhookRequest = req.json().await?;

    let text_lines = Slack.issue_slack_message_text_lines(
        &body.issue,
        &format!("{}/{}#{}", body.repository.owner.login, body.repository.name, body.issue.number),
        &format!("An issue was {}", body.action)
    );

    let slash_command_response = SlashCommandResponse {
        blocks: Slack.construct_gh_issue_slack_message(&body.issue, &text_lines),
        response_type: "in_channel".to_string(),
    };

    let result = Slack
        .send_issue_slack_message(
            &ctx.secret("SLACK_WEBHOOK_URL")?.to_string(),
            &slash_command_response
        )
        .await
        .map_err(|e| worker::Error::RustError(format!("{}", e)));

    match result {
        Ok(_) => Response::ok("OK"),
        Err(e) => Response::error(format!("Unable to handle webhook, message:{:?}", e), 500),
    }
}

Slack表示

20240710214836.png

PullRequest編

次はGithubでPullRequestを作成したら、Slackへ通知するように先程のissue通知を改修してみました。(slashコマンドはなし)
スッキリ書くなら、PullRequest用でわけたほうがよかったかもしれませんが、issueを改修してます。

理由としては以下になります。

  • githubからのissueとPullRequestのリクエスト内容はほぼ同じ(各イベントによって、特有の項目はある)
  • slackに表示したい内容はリンクのURLが異なるぐらいのため

改修

トレイト境界を使って、issueとPullRequestのどちらもいけるようにしました。
※ジェネリクス関数でも可能だったかも。ここらへんはRust力不足。

共通関数

githubからのリクエストを受け取る構造体を下記のように定義しました(一部抜粋)

#[derive(Clone, Serialize, Deserialize)]
pub struct Issue {
  pub title: String,
  pub body: Option<String>,
}

#[derive(Clone, Serialize, Deserialize)]
pub struct PullRequest {
  pub title: String,
  pub body: Option<String>,
}

トレイトに共通の関数を定義し、先程の構造体に実装します

pub trait PayloadRepository {
    fn title(&self) -> &str;
    fn body(&self) -> Option<&str>;
}

impl PayloadRepository for Issue {
    fn title(&self) -> &str {
        &self.title
    }

    fn body(&self) -> Option<&str> {
        self.body.as_deref()
    }
}

impl PayloadRepository for PullRequest {
    fn title(&self) -> &str {
        &self.title
    }

    fn body(&self) -> Option<&str> {
        self.body.as_deref()
    }
}

最後にトレイト境界で、共通関数に型パラメータの制約を追加し、issue型 or PullRequest型でも同じ関数を使って、フィールドの値を取得できるようにしました。

pub fn create_message<T: PayloadRepository>(&self, payload: &T, body: &GithubWebhookRequest) -> Message {
    let text_lines = self.text_lines(
        payload,
        &format!("{}/{}#{}", body.repository.owner.login, body.repository.name, payload.number()),
        &format!("An {} was {}", body.label(), body.action)
    );
}

Slack表示

webhookからのリクエストがissue or Pull requestのどちらでも、slackへ通知されるようになりました。
Pull requestの場合、reviewsの人をメンションさせたいのですが、個人開発のため、reviews指定できる人おらず、この実装はしてません:cry:

20240725234511.png

バッチ編

最後にgithub apiからの取得結果を元に、mrkdwn形式のSlackメッセージにフォーマットして、通知するバッチになります。ざっくりいうと、reqwestクレートを使って、github apiから情報を取得し、slackへpostする感じになってます。

  1. github apiから全リポジトリ取得
  2. リポジトリ一覧をループし、各リポジトリのOpenになってるPullRequestを取得
  3. PullRequestのreviewersとreviewを取得
  4. PullRequestをapprovalにしてないreviewersのmember取得
  5. Slack通知メッセージ形式にフォーマット
  6. Slackへ通知

github api

以下は、github apiへ取得する一部コードです。こんな感じで取得できます。
気をつける点は、headersでUSER_AGENTが必要なことです。これがないとリクエストが失敗する。(仮で適当な値を設定してます。)
参考にしたgithub apiの各サンプルのcurlコマンドにUSER_AGENTはないので、解決するのに時間がかかりました。

pub fn new() -> Self {
    let token = env::var("GITHUB_TOKEN").unwrap();

    let mut headers = HeaderMap::new();
    headers.insert(USER_AGENT, HeaderValue::from_static("request"));
    headers.insert(ACCEPT, HeaderValue::from_static("application/vnd.github.v3+json"));
    headers.insert(AUTHORIZATION, HeaderValue::from_str(&format!("token {}", token)).unwrap());
    headers.insert("X-GitHub-Api-Version", HeaderValue::from_static("2022-11-28"));

    let client = reqwest::Client::new();

    Self { client, headers }
}

pub async fn fetch<T: for<'de> Deserialize<'de>>(&self, url: &str) -> Result<Vec<T>> {
    let response = self.client
        .get(url)
        .headers(self.headers.clone())
        .send()
        .await?;

    match response.error_for_status_ref() {
        Ok(_) => {
            let repos = response.json::<Vec<T>>().await?;
            Ok(repos)
        },
        Err(e) => {
            Err(e.into())
        }
    }
}

Slack表示

20240722001549.png

Github

実際のコードはこちらになります。

issue && PullRequest

バッチ

まとめ & 所感

  • 各連携は簡単
  • すべて無料でできる
  • Rustの勉強になった(ジェネリクス、トレイト境界について、理解が進んだ気がする)

まだまだRustの理解や力不足感は否めませんが、これからもRustのコードをたくさん書いて、もっとレベルアップしたいなと思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?