スタディサプリ Product Team Blog

株式会社リクルートが開発するスタディサプリのプロダクトチームのブログです

dep-doctor による archived / not-maintained な依存ライブラリ検出の仕組み

こんにちは。@chaspyです。技術戦略グループのマネージャをしています。

本記事では dep-doctorという依存ライブラリのメンテナンス状態をチェックするツールを活用した事例を紹介します。

依存ライブラリのメンテナンス状態を確認したい

スタディサプリ小中高では、言うまでもなく多くの OSS / ライブラリに支えられています。しかし、それらのライブラリがメンテナンスされなくなってしまったとしたら、以下のリスクが存在します。

  • セキュリティ: 既知の脆弱性が修正されない可能性があります。新たに発見された脆弱性に対して、パッチが提供されないため、プロジェクトがセキュリティ上の危険にさらされる可能性があります。
  • 互換性: archived / not-maintained なライブラリは、新しいバージョンの言語やフレームワークとの互換性が保証されません。依存ライブラリの更新が滞ると、プロジェクト全体の更新が困難になり、技術的な負債が蓄積する恐れがあります。
  • 機能性: バグや機能の改善が期待できないことも問題です。アクティブな開発が行われていないライブラリでは、既存のバグが修正されず、新機能の追加も見込めません。プロジェクトの機能拡張や品質向上の妨げとなる可能性があります。

これらのリスクを考えると、メンテナンスされてない状態のライブラリを使い続けるのは避けたほうが良いでしょう。そのためには、依存ライブラリのメンテナンス状態を確認することが必要です。dep-doctor を利用することはその1つの手段です。

dep-doctor について

dep-doctor とは、プロジェクトの依存ライブラリを分析し、archive / not-maintained なライブラリを検出するためのツールです。

ツールの紹介については作者様の記事を参照ください。

qiita.com

dep-doctor は、プロジェクトの依存関係を解析し、各ライブラリの状態を自動的にチェックします。GitHub API を利用して、ライブラリのリポジトリ情報を取得し、archive されているかどうかや、リポジトリの最終更新日時を確認します。これにより、プロジェクトで使用されている依存ライブラリのうち、保守が行われていないものを特定することができます。

dep-doctor ではカバーできないこと

dep-doctor は素晴らしいツールです。しかし、実際にこのツールを活用して改善を行う上で、いくつか対応したくなる事象が存在しました。

直接依存のライブラリだけチェックしたい

dep-doctor 自体の特徴であり、長所でもあるのですが、依存ライブラリの依存ライブラリまで調査してくれます。依存ライブラリの依存ライブラリがメンテナンスされていないことを知ることはとても重要です。

しかし、実際にツールを使って診断した際、例えば rails/acriverecord が依存しているライブラリがメンテナンスされてないことが分かったとして、メンテナではない我々にできることはせいぜい Issue を立てることでしょう。あまりアクショナブルな情報ではないと判断し、直接依存だけを判定したくなりました。

調査結果を Slack に通知するために machine-readable な形式で結果を出力したい

運用を考えると、定期実行した結果、サービスのオーナーに archived / not-maintained なライブラリが見つかると通知したくなります。その場合、dep-doctor は単に標準出力に結果が出ますが、machine-readable な形式の方が通知する際には便利です。

解決策

というわけで dep-doctor をラップする + 前述した課題を解決する形でツールを書きました。

github.com

前述した課題に加えて、スタディサプリ小中高では monorepo を採用していることもあり、monorepo でぶん回すことや、特に Ruby について Gemfile.lock で内製ライブラリを省略する処理など細かいところを対応しています。

実際、先ほどあげた課題については dep-doctor 自身への提案*1 でも対応可能ではあると思いますが、ツールそのものの思想にも関わるし、フィードバックするとしても実際に課題を解決し、動くものを作ってからの方が説得力もあると思い、まずツールを書いて運用開始してみることにしました。

archived / not-maintained なライブラリの対処

実際に monorepo 上で動かしてみると、20件程度 archived / not-maintained なライブラリが見つかりました。

サービスオーナーに判断を委ねるために、一度 Spreadsheet に書き出して調査を進めました。調査は自分でも見たものもありますが、半分以上チームに調査していただきました。

見ていくと、使われていないものが合計6件ありました。これらには削除の PR を出しました。使われているものでも、test で使われているものだったり、機能的に十分枯れているものは許容と判断し、ignore list に追加する対応をしました。アーカイブされていた sass/ruby-sass は @yskttm がシュッと sassc に置き換えてくれました。ありがとうございました!

その他やったこと

fork されているリポジトリに対する対応

dep-doctor は内部で GitHub api を実行しています。この検索 api では fork されているリポジトリは検索に引っかかりません。そのため、fork されたリポジトリも検索対象に含まれるよう PR を投げて、取り込んでいただきました。

github.com

gemspec の更新

dep-doctor は Ruby Gem の場合、https://rubygems.org/api/v1/gems/api を実行しており、source code url というメタデータを頼りに Repository を特定しています。このメタデータは gemspec の metadata.source_code_uri として定義できます。これが書かれていないライブラリは "unknown url" という判定になってしまいます。

guides.rubygems.org

さすがに全部修正するのは難しいですが、目についたものは PR を出しました。

github.com

dep-doctor を定期実行する

今後新たに archived / not-maintained なライブラリが出てきた時に、気づけるように GitHub Actions で定期実行して Slack に通知するようにしました。

  • workflow
name: dependency-alerts-slack-notification

on:
  schedule:
    - cron: "0 1 * * 1-5"
  # To test the behavior of this workflow in PR, please comment in
  # pull_request:
  #   branches:
  #     - develop
  #   paths:
  #     - .github/workflows/dependency-alerts-slack-notification.yml
  #     - scripts/dep-doctor-slack-notification.sh

jobs:
  notify:
    timeout-minutes: 20
    runs-on: monorepo-arm64-low-cost # self-hosted runner
    env:
      BOT_USER_OAUTH_TOKEN: ${{ secrets.DEP_DOCTOR_BOT_USER_OAUTH_TOKEN }}
      SLACK_WEBHOOK_URL: ${{ secrets.DEP_DOCTOR_SLACK_WEBHOOK_URL }} # notify to #dev-notification-ja
      # To test to #dep-doctor-test channel, use the below
      # SLACK_WEBHOOK_URL: ${{ secrets.DEP_DOCTOR_TEST_SLACK_WEBHOOK_URL }} # notify to #dep-doctor-test
      GITHUB_TOKEN: ${{ github.token }} # To run gh and dep-doctor
    steps:
      - uses: actions/checkout@v4
      - uses: ./.github/actions/aqua # Install dep-doctor
      - run: gh extension install chaspy/gh-monorepo-dep-doctor
      - run: gh monorepo-dep-doctor >> result.csv
        env:
          MAX_CONCURRENCY: "1" # To avoid GitHub Rate Limit
      - run: cat result.csv
      - run: bash scripts/dep-doctor-slack-notification.sh

gh monorepo-dep-doctor >> result.csv の結果として、以下のようなテキストが生成されます。

api/Gemfile,grape-cache_control,not-maintained,https://github.com/karlfreeman/grape-cache_control
api/Gemfile,http_accept_language,not-maintained,https://github.com/iain/http_accept_language
back-office/Gemfile,sass,archived,https://github.com/sass/ruby-sass

これでどのサービスにどのステータスのライブラリがあるかがわかります。この情報を使って Slack に通知します。

  • script
# Slack から User Group List を取得しておく
USER_GROUPS=$(curl -s -H "Authorization: Bearer $BOT_USER_OAUTH_TOKEN" "https://slack.com/api/usergroups.list")

# User Group 名から ID を問い合わせる
get_usergroup_id_by_name() {
  local group_name="$1"
  echo "${USER_GROUPS}" | jq -r --arg GROUP_NAME "$group_name" '.usergroups[] | select(.name==$GROUP_NAME) | .id'
}

# ただし、パッケージマネージャのディレクトリごとに、通知するグループメンションを変化させたい。
# Slack に問い合わせた group の list から照合する
# sapuri-app で存在すればそれを group handle として決定する
# それ以外の特殊ルールは case 文の通り
# マッチしなければ残念ながら k12-web-devs (no owner)
determine_group_handle() {
  local app="$1"
  group_id=$(get_usergroup_id_by_name "sapuri-${app}")
  if [ -n "$group_id" ]; then
    echo "sapuri-${app}"
  else
    case "$app" in
      # case 分は省略
      qall*) echo "qall-maintainers" ;;
      *) echo "k12-web-devs" ;;
    esac
  fi
}

# result.csv を1行ずつ読み取り、オーナーからメンショングループと ID を判定して Slack に通知する
while IFS=, read -r file_path package_name maintenance_status url
do
  app=$(echo $file_path | cut -d'/' -f1)
  group_handle=$(determine_group_handle "$app")
  group_id=$(get_usergroup_id_by_name "$group_handle")
  message=$(cat <<EOF
<!subteam^${group_id}|${group_handle}> ${app} で利用しているパッケージ *${package_name}* が *${maintenance_status}* 状態です。詳細: ${url} 代替を検討するか、使い続けることを許容するかを判断してください。許容する場合は <https://github.com/quipper/monorepo/blob/develop/.gh-monorepo-dep-doctor-ignore|Ignore File> に追記してください。
EOF
)
echo $message
curl -X POST -H 'Content-type: application/json' \
  --data "{\"text\": \"$message\"}" \
  $SLACK_WEBHOOK_URL
done < result.csv

所属組織では、マイクロサービス単位で命名規則に従ってオーナーチームが整えられています。api というサービスであれば sapuri-api というメンションで通知できるというわけです。一部例外はありますが、この仕組みを使って Slack 通知する時にメンションを含んでいます。

結果はこんな感じです。

これにて概ね archived / not-maintained なライブラリに気づけるようになりました。

おわりに / 今後の展望

現状 Ruby までしか対応できてないため、JavaScript と Go の対応まではやりたいと思います。

ライブラリを継続的にメンテナンスする上で、以下の体制が整いました。

  • アップデートに追従できているか: renovate / dependabot の PR 数を計測し、Datadog Dashboard で可視化
  • アップデートされてないライブラリに対応できているか: dep-doctor を monorepo で利用し、Slack 通知できるようにした

今後もシステムの健全化のため、検知・通知の仕組みや可視化を整えていきたいと思います。

最後に、dep-doctor の作者 @kyoshidajp に心から感謝します。ありがとうございました!

*1:例えば、"直接依存のライブラリだけチェックしたい" 問題については、dep-doctor が内部で使っている go-dep-parserparseDirectDeps に相当する機能を使うようにすれば実現できそうですし、"report 形式を machine-readable にする" には report.go で工夫すれば実現できそうです