Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions spec/discord/models_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ describe Discord::Embed do
end

it "drops empty url / icon fields so Discord does not reject them" do
# アラートは footer_icon: "" で来るため、空文字は nil として出さない
# アラートは footer_icon: "" で来るため、空文字は nil として出さない
message = Notify::Message.new(
author_name: "octocat",
author_link: "",
Expand All @@ -72,7 +72,7 @@ describe Discord::Embed do
embed.author.as(Discord::Author).icon_url.should be_nil
embed.footer.as(Discord::Footer).icon_url.should be_nil

# 空文字フィールドはシリアライズされない
# 空文字フィールドはシリアライズされない
parsed = JSON.parse(embed.to_json)
parsed["footer"].as_h.has_key?("icon_url").should be_false
end
Expand Down
6 changes: 3 additions & 3 deletions spec/github/models_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ describe Github::Subject do
end

it "returns empty for types without a comment body when no comment url is present" do
# CI 完了通知(CheckSuite)などは subject.url を本文取得に使わない
# CI 完了通知(CheckSuite)などは subject.url を本文取得に使わない
subject_from("CheckSuite", url: "https://api.github.com/repos/o/r/check-suites/1").comment_url.should eq ""
end

Expand Down Expand Up @@ -99,7 +99,7 @@ describe Github::Subject do
end

it "returns nil for types whose trailing number is not a GitHub issue/PR number" do
# Release は末尾が数値 ID でも #番号 表示は誤解を招くため付けない
# Release は末尾が数値 ID でも #番号 表示は誤解を招くため付けない
subject_from("Release", url: "https://api.github.com/repos/o/r/releases/5").number.should be_nil
end

Expand Down Expand Up @@ -206,7 +206,7 @@ describe Github::Notification do
end

it "keeps the assign message when the fetched payload carries no comment count" do
# 本文取得に失敗した場合など、判断材料が無いときは初回向け文言のままにする
# 本文取得に失敗した場合など、判断材料が無いときは初回向け文言のままにする
pull_request_without_comment_signal.reason_message(subject_detail).should eq "アサインされました"
end
end
Expand Down
2 changes: 1 addition & 1 deletion spec/notify/usecase_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ describe Notify::Usecase do
it "未送信通知と同一タイムスタンプの通知を巻き込んで既読化しない" do
# 先頭 2 件が同時刻。チャンク境界がその間に落ちても、境界 t(1) は排他的
# なので未送信側の t(1) は既読化されない(送信済み側の t(1) も未読に残り
# 次回再送されるが、ロストよりも稀な重複を許容する)。
# 次回再送されるが、取りこぼしよりも稀な重複を許容する)。
notifications = [notif("2026-01-01T00:00:01Z"), notif("2026-01-01T00:00:01Z"), notif("2026-01-01T00:00:02Z")]
repo = run(notifications, ChunkPoster.new([1, 1, 1])) do |usecase|
usecase.check_notifications
Expand Down
2 changes: 1 addition & 1 deletion spec/slack/models_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe Slack::Attachment do
attachment = Slack::Attachment.from_message(message)

attachment.pretext.should eq "<!channel> hello"
# fallback は生の pretext を保持する
# fallback は生の pretext を保持する
attachment.fallback.should eq "hello"
end

Expand Down
6 changes: 3 additions & 3 deletions src/discord/models.cr
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ module Discord
posts
end

# botのセリフ(pretext)を content に出力する。embed には pretext 相当の欄が
# 無いため、Slack と同様に「botの発言行」として embed の外(content)へ出す
# bot のセリフ(pretext)を content に出力する。embed には pretext 相当の欄が
# 無いため、Slack と同様に「bot の発言行」として embed の外(content)へ出す
# (issue #95)。メンションは embed 内では機能しないため content 先頭に
# @everyone を添える。チャンク内で重複するセリフは uniq でまとめ、content は
# 2000 文字上限があるため truncate する。
Expand Down Expand Up @@ -105,7 +105,7 @@ module Discord
end

def self.from_message(message : Notify::Message) : Embed
# pretext(botのセリフ)は Post の content 側に出すため、description には
# pretext(bot のセリフ)は Post の content 側に出すため、description には
# 含めずコメント本文のみとする(二重表示を避ける / issue #95)。
description = message.text.try(&.presence)

Expand Down
2 changes: 1 addition & 1 deletion src/discord/repository.cr
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ module Discord

# 通知が複数投稿に分割される場合、前半チャンク送信後に後半が 429/5xx で
# 失敗すると既読化されず、次回実行で前半が重複投稿される。これを避けるため
# レート制限・一時的な 5xx は Retry-After に従って再送する。
# レート制限(429)と一時的な 5xx は Retry-After に従って再送する。
retryable = res.status.code == 429 || res.status.server_error?
if retryable && attempt < MAX_SEND_ATTEMPTS
sleep retry_after(res)
Expand Down
18 changes: 9 additions & 9 deletions src/github/models.cr
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
require "json"

module Github
# PR の CI・自動チェックの集計状態(issue #105)。
# PR の CI などの自動チェックの集計状態(issue #105)。
# check runs(GitHub Actions 等)と commit status の 2 系統をまとめて表す。
enum ChecksState
Success # 全て完了し、ブロックする結果が無い
Expand All @@ -23,7 +23,7 @@ module Github
end

# メンションを抑止すべき状態か。
# 成功チェック未設定・取得失敗ではメンションする。取得できなかった場合に
# 成功チェック未設定、取得失敗のいずれでもメンションする。取得できなかった場合に
# 抑止すると通知の見逃しにつながるため、安全側(誤メンションを許容)に倒す。
def blocks_mention? : Bool
failure? || pending?
Expand All @@ -44,8 +44,8 @@ module Github
# "ci_activity",
}

# reason(なぜ自分に通知されたか)ごとの表示文言。update? による
# 「更新があったみたいです」一辺倒だと通知理由が伝わらないため、reason を
# reason(なぜ自分に通知されたか)ごとの表示文言。以前は update? に応じた
# 「更新があったみたいです」の一辺倒で通知理由が伝わらなかったため、reason を
# 文面に反映する(issue #96)。GitHub 側の reason 追加に耐えるよう、
# 未知の reason は reason_message で汎用文言にフォールバックする。
REASON_MESSAGES = {
Expand All @@ -64,7 +64,7 @@ module Github
# 「一度きりの出来事」を指す reason 向けの、2 回目以降の文言。
#
# GitHub の reason は「そのスレッドを購読している理由」であってイベント種別
# ではないため、一度レビュー依頼/アサインされた PRIssue は、以降のコメントや
# ではないため、一度レビュー依頼やアサインを受けた PR / Issue は、以降のコメントや
# 更新もすべて同じ reason で届く。REASON_MESSAGES だけだと常に「レビューを依頼
# されました」「アサインされました」になり通知理由が実態と合わないので、
# 初回ではないと判断できる通知は文言を差し替える(issue #104)。
Expand Down Expand Up @@ -110,7 +110,7 @@ module Github
#
# Subject#commented?(latest_comment_url が subject.url と異なる)だけでは
# 取りこぼす。latest_comment_url は通知を発生させたイベント側を反映することが
# あり、コメント済みのスレッドでも push・レビュー・アサイン変更が起点の通知では
# あり、コメント済みのスレッドでも push やレビュー、アサイン変更が起点の通知では
# subject.url に戻る。またレビューコメントは latest_comment_url に現れない
# ことがあるため、レビュー上でだけ議論されている PR は常に初回扱いになる。
# 結果、コメントの付いた PR でも「アサインされました」のままになる(issue #116)。
Expand All @@ -129,7 +129,7 @@ module Github
detail.commented?
end

# CI・自動チェックの状態でメンションを抑止する対象か(issue #105)。
# CI などの自動チェックの状態でメンションを抑止する対象か(issue #105)。
# レビューできる状態になっていない PR で `@channel` / `@everyone` を撃たない
# ことが目的なので、PR の通知はすべて対象にする。
#
Expand All @@ -146,7 +146,7 @@ module Github
subject.type == Subject::Type::PULL_REQUEST
end

# 通知の pretext(botのセリフ)。`[<type>] <reason 文言>` 形式。
# 通知の pretext(bot のセリフ)。`[<type>] <reason 文言>` 形式。
# detail は reason_message にそのまま渡す(issue #116)。
def pretext(detail : Comment? = nil) : String
"[#{subject.type}] #{reason_message(detail)}"
Expand Down Expand Up @@ -292,7 +292,7 @@ module Github
end

# スレッドにコメントが 1 件以上付いているか(issue #116)。
# 件数が取れない場合(コメントオブジェクト本文取得失敗本文なし通知)は
# 件数が取れない場合(コメントオブジェクト本文取得失敗本文なし通知)は
# 判断材料が無いので false を返し、呼び出し側で初回向け文言に倒す。
def commented? : Bool
total = (comments || 0) + (review_comments || 0)
Expand Down
18 changes: 9 additions & 9 deletions src/github/repository.cr
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ require "../runtime/lambda"
module Github
class NotificationRepository
PER_PAGE = 100 # GitHub /notifications の per_page 上限
MAX_PAGES = 20 # 暴走防止の取得ページ数上限(= 最大 2000 件)
MAX_PAGES = 20 # 暴走防止の取得ページ数上限(最大 2000 件)

def initialize(@token : String)
uri = URI.parse "https://api.github.com"
Expand All @@ -30,21 +30,21 @@ module Github
# 安定させる。上限より新しい通知は取得対象から外れるが、次回実行で取得される。
# before は呼び出し側から受け取る。全件送信後の既読化境界(last_read_at)に
# 同じ値を使うことで、取得フィルタ(updated < before)と既読化(updated <
# last_read_at)が同じ排他的比較になり、「取得・送信した集合」と「既読化される
# last_read_at)が同じ排他的比較になり、「取得して送信した集合」と「既読化される
# 集合」を一致させられる(issue #100)。
#
# 取得しきれない場合(途中ページの一時失敗 5xx/401、またはページ数上限到達)は、
# 不完全な取得状態で既読化して取りこぼすのを避けるため、その実行を丸ごと
# スキップして次回に委ねる。
def find_notifications_unread(before : Time) : Array(Notification)
# ウォームスタート間で使い回した keep-alive 接続が GitHub 側の古い
# レプリカに固定され、未読があるのに空応答が約47分続く事象を観測した
# レプリカに固定され、未読があるのに空応答が約 47 分続く事象を観測した
# (issue #102)。実行のたびに接続を張り直して固定を解き、古い応答を
# 読み続ける時間を最長でも1実行間隔(1分)に抑える。同一実行内の
# ページング・コメント取得・既読化ではそのまま再利用される
# 読み続ける時間を最長でも 1 実行間隔(1 分)に抑える。同一実行内の
# ページングやコメント取得、既読化ではそのまま再利用される
# (close 後の接続は次のリクエストで自動的に張り直される)。
# TLS ハンドシェイクが毎実行1回増えるが、毎分・数百 ms の処理なので
# 通知が長時間届かないリスクより軽いと判断した。
# TLS ハンドシェイクが毎実行 1 回増えるが、毎分の実行が数百 ms で済む
# 処理なので、通知が長時間届かないリスクより軽いと判断した。
@github.close

notifications = [] of Notification
Expand Down Expand Up @@ -137,7 +137,7 @@ module Github
# 100 を超えるチェックは想定しにくいためページングはしない(issue #105)。
CHECKS_PER_PAGE = 100

# PR の CI・自動チェックの集計状態を返す(issue #105)。
# PR の CI などの自動チェックの集計状態を返す(issue #105)。
#
# check runs(GitHub Actions 等)と commit status(外部 CI 等)は別系統で、
# 片方にしか結果が出ないことがあるため両方を見て厳しい方を採る。
Expand Down Expand Up @@ -187,7 +187,7 @@ module Github
end

# チェック状態の取得用 GET。1 件の取得失敗で通知全体を巻き添えにしないよう、
# 例外・エラー応答はログだけ残して nil を返す(issue #105)。
# 例外もエラー応答もログだけ残して nil を返す(issue #105)。
private def get_body(path : String) : String?
res =
begin
Expand Down
4 changes: 2 additions & 2 deletions src/github/usecase.cr
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ module Github

# メンション(`@channel` / `@everyone`)を付けるか。
#
# mention 系 reason であることに加え、PR は CI・自動チェックが失敗中・実行中
# でないことを条件にする。まだレビューできる状態ではない PR でチャンネル全体を
# mention 系 reason であることに加え、PR は CI などの自動チェックが失敗中でも
# 実行中でもないことを条件にする。まだレビューできる状態ではない PR でチャンネル全体を
# 叩かないため(issue #105)。通知そのものは抑止しない。
private def mention?(notify : Notification) : Bool
return false unless notify.mention?
Expand Down
8 changes: 4 additions & 4 deletions src/notify/usecase.cr
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ module Notify

def check_notifications
# 取得スナップショット。取得フィルタ(before)と全件送信後の既読化境界
# (last_read_at)に同じ値を使うことで、取得・送信した集合と既読化される
# (last_read_at)に同じ値を使うことで、取得して送信した集合と既読化される
# 集合を一致させる(issue #100)。
# 秒に切り詰めるのは、両者の一致をシリアライズ精度(現状はどちらも秒単位の
# RFC 3339)に依存させないため。サブセカンドの解釈差による取りこぼしを
Expand Down Expand Up @@ -45,15 +45,15 @@ module Notify
#
# PUT /notifications の last_read_at は排他的境界で、updated_at < last_read_at
# のスレッドだけが既読化される(等値は未読のまま残る。issue #100 で実 API
# 検証済み)。これを前提に境界を選ぶ:
# 検証済み)。これを前提に境界を選ぶ
# - 未送信が残る場合: 境界は未送信先頭の updated_at。排他的なので未送信先頭
# 自身は巻き込まれず、それより前に更新された送信済みは全て既読化される。
# 同一秒がチャンク境界を跨いだ送信済み分は未読に残り次回再送される
# (稀な重複を許容して通知ロストを避ける)
# (稀な重複を許容して通知の取りこぼしを避ける)。
# - 全件送信済みの場合: 境界は取得スナップショット fetched_at。送信済み最新の
# updated_at を境界にすると排他的比較でその通知自身が既読化されず毎分再送
# され続けるため(issue #100 の症状)、取得フィルタと同じ fetched_at まで
# 進める。スナップショット以降の新着は未読のまま次回取得される
# 進める。スナップショット以降の新着は未読のまま次回取得される
private def mark_read_through(notifications : Array(Github::Notification), sent_count : Int32, fetched_at : Time)
return if sent_count <= 0

Expand Down
Loading