背景
このプロジェクトは GitHub 通知を Slack に転送する AWS Lambda(カスタムランタイム)。shard.yml は crystal: 1.11.2 指定だが、実ビルドは crystallang/crystal:latest(現在 1.20.2)の Docker で行われており、コードは古いイディオムのまま。最新の Crystal らしいスタイルに合わせて全面的にリファクタリングする。外部から見た挙動は変えない。
調査で見つかった問題:
src/runtime/lambda.cr の print_log が puts `echo '#{...}'` とサブシェル経由でログ出力しており、APIレスポンスボディがそのままシェルに渡る(コマンドインジェクション/クォート破壊リスク)
spec/spec_helper.cr が存在しない ../src/github_notifications_slack を require しておりスペックはコンパイル不能。スペック本体も false.should eq(true) のプレースホルダ
crystal tool format --check で lambda.cr / slack/models.cr に差分あり
- shard.lock に未使用の
clim が残存、.travis.yml は死んだ CI の残骸
src/github/usecase.cr で attachment 1件ごとに NotificationRepository.new(= HTTP::Client 生成)している N+1
Phase 1: イディオム・スタイル最新化
shard.yml
src/runtime/lambda.cr
src/github/repository.cr
src/github/models.cr
src/github/usecase.cr
src/notify/usecase.cr
src/error/usecase.cr
src/slack/models.cr
Phase 2: 設計改善
Phase 3: ツーリング整備
検証
crystal tool format --check src spec が差分なし
crystal build src/main.cr がコンパイル成功(static リンクは Linux ビルド時のみなので通常 build で確認)
crystal spec が green
bin/ameba が指摘なし
- ロジック差分を最終レビューし挙動不変を確認(特に 401 スキップ・5xx スキップ・既読化の条件分岐)。実環境確認は serverless-dev デプロイに委ねる
背景
このプロジェクトは GitHub 通知を Slack に転送する AWS Lambda(カスタムランタイム)。shard.yml は
crystal: 1.11.2指定だが、実ビルドはcrystallang/crystal:latest(現在 1.20.2)の Docker で行われており、コードは古いイディオムのまま。最新の Crystal らしいスタイルに合わせて全面的にリファクタリングする。外部から見た挙動は変えない。調査で見つかった問題:
src/runtime/lambda.crのprint_logがputs `echo '#{...}'`とサブシェル経由でログ出力しており、APIレスポンスボディがそのままシェルに渡る(コマンドインジェクション/クォート破壊リスク)spec/spec_helper.crが存在しない../src/github_notifications_slackを require しておりスペックはコンパイル不能。スペック本体もfalse.should eq(true)のプレースホルダcrystal tool format --checkでlambda.cr/slack/models.crに差分ありclimが残存、.travis.ymlは死んだ CI の残骸src/github/usecase.crで attachment 1件ごとにNotificationRepository.new(= HTTP::Client 生成)している N+1Phase 1: イディオム・スタイル最新化
shard.yml
crystal: ">= 1.20.0"に更新(ビルドは latest Docker なので整合)src/runtime/lambda.cr
print_logのputs `echo '...'`を stdlibLogに置換(Log.for("lambda")等)。改行除去(CloudWatch で1エントリ化するため)と 50,000 文字チャンク分割の意図はeach_char.each_sliceベースで維持し、シェル呼び出しだけ排除def handler(name : String)→ 明示ブロックパラメータdef handler(name : String, &)src/github/repository.cr
HTTP::Statusの述語メソッドへ(server_error?/unauthorized?/client_error?)。401スキップの意図コメントは維持Array(Notifications).new→[] of Notificationretrun→return、faild→failedServerless::Lambda.print_log呼び出しをLogへsrc/github/models.cr
property→getter@[JSON::Field(emit_null: false)]はデフォルト挙動なので削除update?/mention?の[...].includes?(x)→ 定数配列 +x.in?(CONST)comment_url→latest_comment_url.presence || urlSubject::Typeの enum 化は やらない(GitHub が未知 type を返すと from_json が落ちるため、文字列定数を維持)src/github/usecase.cr
!notify.repository.full_name.nil? ? notify.repository.full_name : "github"→notify.repository.full_name || "github"src/notify/usecase.cr
if notices.size != 0→unless notices.empty?src/error/usecase.cr
err.backtrace→err.backtrace?.try(&.join('\n'))(backtrace 未設定時の例外を防止)src/slack/models.cr
property→getter(シリアライズ専用)Phase 2: 設計改善
Github::Notifications→Github::Notification(1件の通知を表すため単数形)。find_notifications_unreadの返り値型も追随Github::Usecase#to_slack_attachment内のNotificationRepository.newをやめ、コンストラクタ注入Github::Usecase.new(repo : NotificationRepository)に。HTTP::Client が全通知で1つになるENV["GITHUB_TOKEN"]/WEBHOOK_URL/SLACK_IDなどの参照をsrc/main.cr(composition root)に集約し、ENV.fetchで必須変数の欠如を起動時に検出。Notify::Usecase/Error::Usecaseはコンストラクタで依存を受け取るsend_attachmentはsend_attachments([attachment])への委譲に統一。@urlを保持せず@uriのみにPhase 3: ツーリング整備
development_dependenciesにameba(crystal-ameba/ameba,~> 1.6)を追加 →shards install(副作用で stale なclimも shard.lock から消える)→bin/amebaの指摘に対応。必要なら.ameba.ymlで調整Subject#color/#update?/#comment_url、Notification#mention?、JSON パース)。HTTP 層のモックは今回は導入しない.github/workflows/ci.ymlを新規作成。crystal-lang/install-crystalアクションで latest を入れ、crystal tool format --check/bin/ameba/crystal specを PR と master push で実行.travis.yml削除検証
crystal tool format --check src specが差分なしcrystal build src/main.crがコンパイル成功(static リンクは Linux ビルド時のみなので通常 build で確認)crystal specが greenbin/amebaが指摘なし