同僚のSlackレビュー通知botに「営業時間外は翌朝に通知を遅延する」機能を提案・実装した

同僚が開発しているSlackのレビュー通知bot slack-review-notify を使っていて、re-review通知だけが営業時間制御の対象外であることに気づきました。保留→蓄積→翌朝一括送信の仕組みを提案・実装し、PRを出した話です。設計判断やE2Eテスト戦略に加えて、同僚のOSSに機能を提案するときに意識したことも書きます。

slack-review-notifyとは

slack-review-notifyは、GitHubのPRレビューリクエストをSlackに通知するbotです。同僚の@haruotsuくんが開発・公開しています。

GitHub - haruotsu/slack-review-notify: A Slack bot that notifies channels on GitHub PR label events, randomly assigns reviewers, sends periodic reminders within business hours, and handles off-hours queuing. Configurable entirely via Slack commands.
A Slack bot that notifies channels on GitHub PR label events, randomly assigns reviewers, sends periodic reminders withi...

主な機能はこちら。

  • PRにラベルを付けるとSlackに通知
  • レビュアーをランダムに自動アサイン
  • 未レビューのPRをリマインド
  • 営業時間の設定(チャンネルごと)
  • レビュー完了の自動検知とお礼メッセージ
  • 休暇管理
  • 日本語 / 英語の切り替え

Slackコマンドだけで設定が完結するので、導入もかんたんです。

Review Flow

課題: 深夜のre-review通知

このbotにはすでに「営業時間外にラベルが付いたPRは翌朝まで通知を保留する」機能がありました。ところが、re-review(レビュー修正後の再レビュー依頼)だけはこの制御の対象外でした。

深夜に作業してGitHub上のre-reviewボタンを押すと、レビュアーのSlackへ即座に通知が飛びます。深夜に対応する必要はないのに、通知自体は来てしまう。チームで使っていて気になったので、改善を提案しました。

設計: 保留 → 蓄積 → 翌朝一括送信

通知を止めるだけなら単純ですが、以下の要件を満たす必要がありました。

  1. 送信者へのフィードバック: re-reviewを送った人に「営業時間外なので翌朝通知します」と即座に伝える
  2. 複数回の蓄積: 1つのPRに対して複数人が営業時間外にre-reviewする可能性がある
  3. 翌営業日の朝に確実に送信: 1分間隔のタスクチェッカーで営業時間開始を検知する
  4. 並行更新への耐性: タスクチェッカーとWebhookハンドラが同時に同じタスクを更新する可能性がある

データモデル

ReviewTaskモデルに3つのフィールドを追加しました。

type ReviewTask struct {
    // ... 既存フィールド ...
    PendingReReviewNotify   bool   // 保留中フラグ
    PendingReReviewSender   string // 送信者(カンマ区切りで蓄積)
    PendingReReviewReviewer string // レビュアー(カンマ区切りで蓄積)
}

複数回のre-reviewは <@U123>,<@U456> のようにカンマ区切りで蓄積します。専用テーブルを作る選択肢もありましたが、既存のタスクチェッカーの仕組みに乗せるにはフィールド追加のほうがシンプルでした。

処理フロー

Webhookハンドラ(re-review受信時)
├─ 営業時間内 → 即座に通知(従来どおり)
└─ 営業時間外
   ├─ pending_re_review_notify = true
   ├─ sender/reviewerをカンマ区切りで追記(CAS付き)
   └─ 「営業時間外のため翌営業日の朝に通知します」をスレッドに投稿

タスクチェッカー(1分間隔)
├─ pending_re_review_notify = true のタスクを検索
├─ 営業時間内かチェック
├─ sender/reviewerのペアごとに通知を送信
└─ フラグをクリア(CAS付き)

CASパターンで並行更新を防ぐ

処理フローを見ると、Webhookハンドラによるフラグのセットとタスクチェッカーによるクリアのタイミングで競合が起こりえます。排他制御なしだと、タスクチェッカーがフラグをクリアした直後にWebhookハンドラが新しいre-reviewをセットする場合があります。このとき、先にセットされた内容が失われます。

この問題に対してCAS(Compare-And-Swap)パターンを採用しました。UPDATE文のWHERE句にupdated_atを含めることで、読み取り時と更新時の間にレコードが変更されていないことを保証します。

// CAS: updated_at が変わっていなければ更新する
result := db.Model(&models.ReviewTask{}).
    Where("id = ? AND updated_at = ?", taskID, expectedUpdatedAt).
    Updates(map[string]interface{}{
        "pending_re_review_notify":   false,
        "pending_re_review_sender":   "",
        "pending_re_review_reviewer": "",
        "updated_at":                 now,
    })
if result.RowsAffected == 0 {
    log.Printf("CAS miss (concurrent update): task=%s", taskID)
}

RowsAffected == 0 のときは別のプロセスが先に更新しています。この場合は次回のチェックサイクル(1分後)で再処理されるので、データが失われることはありません。

データベースレベルのロック(SELECT ... FOR UPDATE)を使わない理由は2つあります。このbotがSQLiteで動いていること、そして1分間隔のポーリングなので「次のサイクルで拾えばよい」という割り切りができることです。

slackhogでE2Eテストを書く

Slack botのテストは厄介です。実際のSlack APIを叩くわけにはいかないし、モックだけでは通知が本当にスレッドに届くかわかりません。

同僚の@harakeishiくんがまさにこの課題に対してslackhogというSlack APIエミュレータを開発・公開していたので、今回のPRではこれを使ってE2Eテストを書きました。Dockerコンテナとして起動するだけでSlack互換のAPIが手に入り、通知の到達までテストコード内で検証できます。

GitHub - harakeishi/slackhog: A MailHog-like tool for Slack. SlackHog catches Slack API requests locally and displays them in a Slack-like Web UI — useful for developing and testing Slack integrations without sending real messages.
A MailHog-like tool for Slack. SlackHog catches Slack API requests locally and displays them in a Slack-like Web UI — us...

テストの構成

# slackhog.e2e.yaml
port: 4112
max_messages: 1000
channels:
  - C_E2E_BH
  - C_E2E_NOBH
# Makefile
e2e: e2e-setup
    $(GO) test -tags e2e -v -count=1 . || ($(MAKE) e2e-teardown && exit 1)
    $(MAKE) e2e-teardown

-tags e2e でビルドタグを分離しているので、通常の go test ではE2Eテストは実行されません。

テストシナリオ

3つのシナリオを検証しました。

シナリオ 検証内容
営業時間内のre-review slackhogのスレッドに即座に通知が到達する
営業時間外のre-review → 営業時間開始 保留フラグがセットされ、チェッカー実行後にスレッドに到達する
営業時間外に複数回re-review → 営業時間開始 sender/reviewerが蓄積され、2件のスレッド返信として到達する

ここまでで機能の設計・実装・テストの全体像が揃いました。以下では営業時間外のケースを例に、テストの具体的な流れを見ていきます。

// 1. 営業時間を極端に狭く設定(03:00-03:01)→ ほぼ確実に営業時間外
config := models.ChannelConfig{
    BusinessHoursStart: "03:00",
    BusinessHoursEnd:   "03:01",
    Timezone:           "Asia/Tokyo",
}

// 2. Webhookを送信 → 保留される
resp := sendWebhook(t, ts.URL, payload)

// 3. フラグがセットされていることを確認
assert.True(t, deferredTask.PendingReReviewNotify)

// 4. 営業時間設定を変更して「今は営業時間内」にする
db.Model(&config).Updates(map[string]interface{}{
    "business_hours_start": "00:00",
    "business_hours_end":   "23:59",
})

// 5. チェッカーを手動実行
services.CheckPendingReReviewNotifications(db)

// 6. slackhogのスレッドに通知が到達していることを確認
replies := getSlackhogReplies(t, parentID)

ポイントは「営業時間設定を動的に変更する」ことです。テスト時刻へ依存しない設計なので、実時刻を操作するライブラリは不要でした。

slackhogの確認用API

slackhogにはメッセージの確認用APIがあり、チャンネルごとのメッセージ一覧やスレッドの返信を取得できます。

// チャンネルのメッセージ一覧
GET /_api/messages?channel=C_E2E_BH

// 特定メッセージのスレッド返信
GET /_api/messages/{parentID}/replies

Web UIも用意されていて、テスト実行中にブラウザで http://localhost:14112 を開くとSlack風の画面でメッセージを確認できます。

slackhog E2Eスクリーンショット

同僚のOSSに機能提案するときに意識したこと

今回の変更はPR #92として提出しました。同僚のプロダクトに機能を足すにあたって意識した点を振り返ります。

既存の設計に乗る。新しいテーブルやgoroutineを追加せず、既存の ReviewTask モデルと1分間隔のタスクチェッカーの仕組みに乗せました。レビュアーが把握すべき新しい概念を減らすためです。

テストを手厚くする。自分のリポジトリなら「動いたからOK」で進めるところを、ユニットテスト8件 + E2Eテスト3件を書きました。CASのヒット/ミス、設定が消えた場合のフォールバック、完了済みタスクのスキップなどエッジケースも網羅しています。

「なぜこの設計か」を説明する。PR descriptionにはCASパターンを選んだ理由、カンマ区切りにした判断、テストシナリオの一覧を記載しました。コードだけでは伝わらない設計意図をレビュアーが追えるようにするためです。

なお、今回の実装はClaude Codeを活用して進めました。同僚の大事なプロダクトに手を入れるからこそ、設計方針やエッジケースの洗い出しは自分で判断しつつ、コーディングやテスト作成はClaude Codeに任せて石橋を叩くように進めました。

まとめ

  • slack-review-notifyはGitHub PRのレビューフローをSlackで管理するbot。営業時間制御やリマインダーなど実用的な機能が揃っている
  • re-review通知の営業時間外での遅延は、保留フラグ + カンマ区切り蓄積 + CASパターンで実装した
  • Slack botのE2Eテストにはslackhogが便利。Docker起動だけでSlack互換APIが手に入り、メッセージの到達確認までテストコードで完結する
  • 同僚のOSSに機能を提案するときは、既存設計に乗ること、テストを手厚くすること、設計意図を明文化することが大事
GitHub - haruotsu/slack-review-notify: A Slack bot that notifies channels on GitHub PR label events, randomly assigns reviewers, sends periodic reminders within business hours, and handles off-hours queuing. Configurable entirely via Slack commands.
A Slack bot that notifies channels on GitHub PR label events, randomly assigns reviewers, sends periodic reminders withi...
re-review通知の営業時間外遅延機能を追加 by atani · Pull Request #92 · haruotsu/slack-review-notify
Summaryre-review通知(GitHub上のre-requestボタン押下時)が営業時間外に発生した場合、翌営業日朝まで遅延させる。ReviewTaskモデルに保留フラグとメンション情報を保持するフィールドを追加する。1分ごとのタ...

コメント

タイトルとURLをコピーしました