$shibayu36->blog;

株式会社アンドパッドでエンジニアをしています。プログラミングや読書のことなどについて書いています。

GitHub ActionsでRuboCopのキャッシュを利用する

RuboCopはキャッシュファイルを作成し、2度目以降の実行を高速化するのですが、GitHub Actionsでキャッシュを利用するために工夫が必要だったのでメモしておきます。

小規模なプロジェクトの場合

小規模なプロジェクトでRuboCopの実行時間がある程度ありキャッシュを利用したい場合は、キャッシュのvalidityチェックは完全にRuboCopにお任せし、すべてのGitHub Actionsでキャッシュを作ってしまうやり方がオススメです。理由は、小規模な場合キャッシュサイズが問題になるケースは少なく、全部キャッシュした方がシンプルかつメンテナンス性も高いためです。

参考の設定はこちら。

    steps:
      ...
      - name: Cache rubocop
        uses: actions/cache@v3
        env:
          cache-name: cache-rubocop-v1
        with:
          path: ~/.cache/rubocop_cache
          key: ${{ env.cache-name }}-${{ github.head_ref }}
          restore-keys: ${{ env.cache-name }}-
      - run: bundle exec rubocop --parallel
      ...

古いキャッシュが残り続け、キャッシュサイズが肥大化する場合は、https://docs.rubocop.org/rubocop/usage/caching.html を参考に、AllCops: MaxFilesInCache を調整すると良いと思います。

大規模でキャッシュサイズが問題になる場合

大規模なプロジェクト場合、一度のRuboCopのキャッシュが500KB〜1MBに到達するケースがあります。かつ毎回のGitHub Actions実行でキャッシュを保持してしまうと、GitHub Actionsのキャッシュ上限に達してしまう場合があります。その場合はキャッシュの保持の仕方を工夫しなければなりません。ただし保持の仕方を間違えると以下の問題に遭遇します。

  • cache-rubocop-hashhhhdummmmmmy1のようなキャッシュが作られているとする
  • RuboCopのバージョンを更新すると、これまでのすべてのキャッシュが無効になる
  • すべてのキャッシュが使えないため、RuboCopのチェックがフルで実行される
  • 新しくできたキャッシュをActions側のキャッシュに入れようとするが、そちらのキャッシュキーが変わっていないとキャッシュ保存がスキップされてしまう
  • 結果、古いキャッシュを使い続け、ずっとフル実行のままになってしまう

この問題にならないためには、RuboCopで全キャッシュが無効になるタイミングでは、必ずGitHub Actionsのキャッシュキーも変えておく必要があります。https://docs.rubocop.org/rubocop/usage/caching.html#cache-validity を参考にすると、Rubyのバージョンを上げる・RuboCopのバージョンを上げる・RuboCopの設定を変える・rubocopコマンドへ渡すオプションを変えるケースだとすべてのキャッシュが無効になりそうです。

これらを考慮した参考例は次の通り。RuboCopのキャッシュが無効にならないケースでもGitHub Actions側のキャッシュキーが変わる時もありますが、キャッシュサイズの制御のためなので厳密なチェックはしていません。

    steps:
      ...
      - name: Cache rubocop
        uses: actions/cache@v3
        env:
          cache-name: cache-rubocop-v1
        with:
          path: ~/.cache/rubocop_cache
          # RuboCopのcacheのvalidityに合わせてキャッシュキーを決める
          # * RuboCopの設定変更の時にキャッシュを分けるため、.rubocop.ymlと.rubocop_todo.ymlを見る
          # * RuboCopのバージョン更新の時にキャッシュを分けるため、Gemfile.lockを見る
          # * Rubyのバージョンが変更された時にキャッシュを分けるため、.ruby-versionを見る
          key: ${{ env.cache-name }}-${{ hashFiles('.rubocop.yml', '.rubocop_todo.yml', 'Gemfile.lock', '.ruby-version') }}
          restore-keys: ${{ env.cache-name }}-
      - run: bundle exec rubocop --parallel
      ...

このチェックではrubocopコマンドに渡すオプションを変えたときにキャッシュキーを変えていないため、必要があればhashFilesに関係するファイルを入れておくと良さそうです。

またこのやり方の場合、hashFilesに指定したファイルをずっと更新しなかった場合に、徐々にキャッシュが古くなっていき実行が遅くなるという問題があります。しかし、大規模な場合、Gemfile.lockは1~2週に一度は更新されるであろうと考え、あまり気にしていません。様子を見て、もし問題になってきた場合は対策を考える予定です。例えばhttps://jpdebug.com/p/2821562のように、数日に一度はキャッシュがクリアされるように日付をキャッシュキーに入れるやり方を取ると良さそうです。

まとめ

今回はGitHub ActionsでRuboCopのキャッシュを保持する設定について書きました。大規模なプロジェクトの場合、工夫が必要なので難しいですね。

調査メモ: RuboCopのキャッシュ構造について

RuboCopは以下のようなキャッシュ構造になっている。

{cache_root}/{rubocop_checksum}/{context_checksum}/{file_checksum}

たとえば ~/.cache/rubocop_cache/48495939d6d5ca59d7f0a191fd9c11432a988b9d/eb1638cf9f9405f1dfcb03f4d43f86ec4b3f6af5/bab558f367464a4c3a65c3c112fae2e3168613cd のようなファイルに、どんな違反があったかの情報が書かれている。現在RuboCopチェックをかけようとしているファイルのキャッシュがあれば、違反内容は変わらないためキャッシュファイル内に記載された違反内容をそのまま返せば良い。

Rubyのバージョン変更、RuboCopの設定変更、RuboCopのオプション変更はrubocop_checksumやcontext_checksumへ影響を及ぼすので、全キャッシュが無効になるようだ。

まったく何も変更がない場合、checksumの計算 + RuboCopチェックをかける分のキャッシュ読み込みのみ行うため、ほぼ一瞬でRuboCopの結果が返ってくるようだ。

More Effective Agile読んだ

読みました。アジャイル開発やスクラム開発をしている時に取り入れると良いプラクティスを大量に知ることができて良かった。

印象に残ったのは以下の項目。数字はkindle location

  • スクラムの基本は、最も端的にスクラムをどうやるかを理解できる形になっていて良かった 912
  • スクラムを成功させるには、「有能なプロダクトオーナーを割り当てる」「バックログリファインメントを行う」「ストーリーを小さく保つ」「デイリースクラムを毎日行う」「スプリント期間は1~3週」「作業をバーティカルスライスにまとめる」「テスト・テスト技術者・品質保証を開発チームに組み入れる」、「完成の定義の明確化」「各スプリントでリリース可能な品質水準を達成する」「毎スプリントでレトロスペクティブ」「レトロスペクティブからの学びを活かす」「有能なスクラムマスターを割り当てる」
  • 仕事のできる職能横断的チームには、スキル以外に、拘束力のある決定を下す能力と権限の両方が必要 1350
    • 決定を下す能力 = チームに効果的な意思決定を行うのに必要な専門知識が全て揃っている
    • 決定を下す権限 = 組織の他の誰かが覆すことができない決定を下す能力。重要なステークホルダーがチームに参加している
  • 基本原則:成長マインドセットを培う
    • プロジェクトの目的は、動くソフトウェアを作るだけじゃなく、ソフトウェアを作るチームの能力を向上させることもある
  • 銀の弾丸に近いソフトウェアプラクティスは、1人1人の開発者が実際の顧客であるシステムの実際のユーザーと定期的に直接交流する 1645
    • ただし1回限りの体験では、その時に目撃したユーザーに開発者が固執する場合があるので、定期的に行うことが重要
  • 技術的負債の分類法 2413
    • 意図的な技術的負債(短期):たとえば、時間的制約のあるリリースを期限までにデプロイするなど
    • 意図的な技術的負債(長期):たとえば、最初からマルチプラットフォーム対応の設計と構築を行うのではなく、最初は1つのプラットフォームだけをサポート
    • 不慮の技術的負債(悪意):いい加減なソフトウェア開発の習慣のせいで意図せずに発生する技術的負債
    • 不慮の技術的負債(善意):ソフトウェア開発がエラーと隣り合わせの性質であるために意図せずに発生する技術的負債。たとえば「私たちの設計アプローチは思っていたほどうまくいかなかった」「プラットフォームの新しいバージョンのせいで設計の重要な部分が無効になってしまった」
    • レガシーな技術的負債:新しいチームに引き継がれた古いコードベースの技術的負債
  • チーム間で意味のある生産性の比較は、チームの生産性の向上率 4089

スケールする組織を支えるチームタイプやチームインタラクションを学ぶ - チームトポロジー読んだ

読みました。非常に良かったので、スケールする組織をどうやって作るか考えている人は一読すると良さそう。とくに4つのチームタイプ、3つのインタラクションモードについて知れたのが良かった。

  • チームトポロジーは、チームファーストアプローチを適用し、4つの基本的なチームタイプと3つのインタラクションパターンを示す。この方法に従うと、生み出されるソフトウェアのアーキテクチャが明快で持続可能になる。結果的に、チーム間の問題を有用なシグナルとして扱えるようになり、組織の自律操舵を促す
  • 4つの基本的なチームタイプ
    • ストリームアラインドチーム:ビジネスの主な変更フローに沿って配置されるチーム。職能横断型で、他のチームを待つことなく、利用可能な機能をデリバリーする能力を持つ
    • プラットフォームチーム:下位のプラットフォームを扱うチームで、ストリームアラインドチームのデリバリーを助ける。プラットフォームは、直接使うと複雑な技術をシンプルにし、利用するチームの認知負荷を減らす
    • イネイブリングチーム:転換期や学習期に、他のチームがソフトウェアを導入したり変更したりするのを助ける
    • コンプリケイテッド・サブシステムチーム:普通のストリームアラインドチーム、プラットフォームチームが扱うには複雑すぎるサブシステムを扱うためのチーム。本当に必要な場合にだけ編成
  • 3つのインタラクションモード
    • コラボレーションモード:特に新しい技術やアプローチを探索している間、2つのチームがゴールを共有して一緒に働く。学習のペースを加速する上で、このオーバーヘッドには価値がある
    • X-as-a-Serviceモード:あるチームが、別のチームが提供する何かを利用する(API、ツール、ソフトウェア製品全体など)。コラボレーションは最小限になっている
    • ファシリテーションモード:あるチーム(通常はイネイブリングチーム)が、新しいアプローチの学習と適用を促すため、他のチームをファシリテーションする
  • 4つの基本的なチームと3つのインタラクションモードの組み合わせが、速いフローで効果的にデリバリーを行うのに必要。そこから外れるなら考え直した方が良い

この知識を利用して、自分の組織のチームを分類し、より良い組織構造について考えていきたい。