KAKEHASHI Tech Blog

カケハシのEngineer Teamによるブログです。

フロントエンドリポジトリの GitHub Actions で billable time を削減してみた

カケハシの AI 在庫管理でフロントエンド開発を主にしている鳥海 (@toripeeeeee) です。こちらの記事はカケハシ Advent Calendar 2024 の 19 日目の記事になります。

GitHub Actions では実行時間単位で課金されるため、課金対象時間を表す billable time というものがあります。 最近、私が利用しているリポジトリで billable time を削減するために動くことがあったので、そこでの削減に至るまでの取り組みについてご紹介したいと思います。

なぜ billable time 削減に動いたのか?

GitHub Actions では、さまざまな job が何回も動いているもののため、開発が活発になるにつれ、

  • プルリクエストやリリース頻度が増える
  • テストやファイルなどの処理対象が増える

などの要因で billable time は増えるものではあります。とは言いつつもActions Usage Metricsを見てみると、 私が担当しているサービスの billable time がダントツで多い状況であり、私が開発を進めているフロントエンドのリポジトリでもかなり多くなっていました。

そのため、billable time の適正化を図ることで、コストの削減を行うとともに、 テストなどが増えても billable time が増えすぎないようにしたいと考えました。

また、今後サービスが増えていくと考えると、1 つ 1 つのサービスのコストがチリツモで全体の大きなコストになっていくので、 それぞれのサービスで適切に管理することが重要です。

ステップ1: インパクトの高い workflow・job を見つける

Actions Usage Metrics では、リポジトリを指定すれば、そのリポジトリでどの workflow の実行時間が多いのかを表示してくれます。

また、この時に job 単位でも確認しておくと、workflow のどの job に対して重点的に改善していけば良いのかが明らかになり、良いと思います。 これらの情報を元に上位に表示される workflow ファイルでのそれぞれの削減手段を考えていきます。

ステップ 2: 削減手段を考える

billable time を削減する方法としては、

  • 不要な job を削除する
  • 過剰に実行されているものを削減する
  • 実行される処理自体を高速化/効率化する

の 3 点になると思います。それぞれどういうことをしたのかをみていきましょう。

手段 1: うまく利用できていない job を断捨離する

workflow を確認してみると、どうしても管理しきれないもの、使いきれていなものがあると思います。 私のチームでは VRT と Next.js の Bundle Analyzer の job が削除対象となりました。

VRT では、Storycap + reg-suit で管理していたものの、開発が進むにつれて Storybook の story ファイルも多くなっており、失敗頻度も高くなっている状況でした。また、E2E テストやインテグレーションテストの拡充・整理も進んできていることもあり、VRT の重要性も薄れてきていました。その中で、費用対効果を考え、削除する方針となりました。

Next.js の Bundle Analyzer も同様の理由で、自分たちが必要としているのか、そのためにちゃんと管理できているのかを確認し、 削除していきました。

この部分については、自チームの扱うサービスの特性や CI の戦略と照らし合わせながら、適切に意思決定すると良いと思います。

手段 2: 実行タイミングの調整する

不要な job を削除した後、重複して実行されている部分や過剰に実行されている部分を確認し、必要最低限のトリガーに調整します。これにより、ムダのない実行タイミングにします。

実行途中で再実行されたらキャンセルする

GitHub Actions では、下記のように concurrency の設定を加えるだけで、実行途中のものをキャンセルできます。

on:
  pull_request:
    branches:
      - develop
    types: [opened, synchronize, reopened]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

こうすることで、プルリクエストを作成しているときに、プッシュ後にすぐに修正をプッシュした場合の重複実行も制限され、 実行時間を短縮できます。

トリガーを調整する

GitHub Actions では、さまざまなトリガーが用意されていますが、いくつかのトリガーのフィルタリング設定によって実行の調整を行うことができます。

  • branches を指定する
  • paths を指定する
  • job をスキップする

このようなトリガーの調整パターンがあり、実際にこれらを組み合わせて調整を行いました。 順を追ってみていきましょう。

1. branches を指定する

下記のように develop から main に変更するような手段です。 develop で実行せずとも影響があまりないもので、main のみ実行すれば事足りるものについては、 この手段で実行回数を減らすことができると思います。また、何かしら変更して main 以外での実行をしたい場合に備えて、 手動実行用の workflow_dispatch トリガーを用意しておくと良いです。

# before
on:
  pull_request:
    branches:
      - develop
    types: [closed]

# after
on:
  pull_request:
    branches:
      - main # ここ (develop -> main)
    types: [closed]
  workflow_dispatch:

2. paths を指定する

指定したパスのファイルに差分があるときに実行されるようにすることで、実行回数を減らすことができます。 たとえば、monorepo 内で packages の web-app が変更された時だけ実行するだと下記のようになります。

# before
on:
  pull_request:
    branches:
      - main
    types: [closed]

# after
on:
  pull_request:
    branches:
      - main
    paths:
      - packages/web-app/** # ここ (packages/web-app配下で変更があったときの条件を追加)
    types: [closed]
  workflow_dispatch:

3. job をスキップする

最後に if 条件による job のフィルタリングです。 たとえば、下記例のように feature ブランチへのプルリクエストのみ実行しないようにできたりするため、 この設定を入れることでムダな実行を減らすことができます。

# before
on:
  pull_request:
    paths:
      - packages/web-app/**
    types: [closed]
jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    ...

# after
on:
  pull_request:
    paths:
      - packages/web-app/**
    types: [closed]
jobs:
  test:
    if: ${{ !contains(github.base_ref, 'feature/') }}  # ここ (featureブランチへのプルリクエストの時は除外条件を追加)
    runs-on: ubuntu-latest
    timeout-minutes: 15
    ...

手段 3: 実行される処理自体を高速化/効率化する

最後は、GitHub Actions の処理そのものを高速・効率的にします。

  • より高速なパッケージ・ツールへの入れ替え
  • 実処理の効率化
  • キャッシュの効率化

取り組んだ内容としては、このような対応になります。

1. ツールの入れ替え

All TypeScript の Monorepo の linter を ESLint から Biome にしたら lint が 25 倍速くなった 🚀」でも言及されているように、 ESlint から Biome に変更することで実行速度が上がることが期待されます。 実際、私のチームでは ESlint から Biome に変更されることで、5 分かかっていたものが、2 分弱で完了するようになりました。
このように利用するパッケージを変更することで、CI の時間が短縮される場面があるので、 チームの方針で問題がなければ、効果を得られるポイントなので実施してみても良いかもしれないです。

2. 実処理の効率化: テストの改善

フロントエンドリポジトリのテストの効率化に関しては、下記事項を確認すると良いと思います。

  • CI 上で利用できる分の worker 数を設定できているか?
  • ムダなコンポーネントのレンダリングなどで効率が悪くなっていないか?

実際に、私のチームでは worker をしっかり使えていなかったために、実行時間が長くなるようなこともありました。 また、ダイアログのインテグレーションテストの実行時にダイアログ以外も含めてレンダリングしているので、その分時間がかかっている部分もありました。 こういったこともあったので、このような観点でテスト実行の効率化を図ると良いかもしれないです。

3. キャッシュの効率化

私のチームでは、node_modules をキャッシュし、CI のさまざまな workflow でそのキャッシュを利用しています。 このキャッシュサイズをより小さくすることで、少しでも保存と復元を効率的に行うことができます。 キャッシュを小さくするアプローチは他にもあると思いますが、私のチームのリポジトリでは renovate の設定において、pnpm dedupe する設定を追加しました。 下記が設定例となります。

{
  ...
  "postUpdateOptions": ["pnpmDedupe"],
}

dedupe コマンドはリポジトリ内の依存関係を整理し、重複しているパッケージを削除することで依存関係を最適化してくれます。 そのため、このように dedupe を定期的に行うような設定を行うことで、GitHub Actions 内で利用するキャッシュサイズを減らし、 より効率的に CI を実行できます。

結果

最終的に色々と組み合わせ取り組んだ結果、9 月の状況に比べて、開発状況の変化はあったものの、リポジトリの billable time 合計を 45000 分 → 25000 分に削減することができました。

また、副次的な効果で GitHub Actions の workflow を改善を通して、CI が完了する時間についても短縮していきたいという気持ちも出てきました。そこで、10 分以内に完了するコミットラインを設定し、並列数などの調整をすることで、完了時間にもコミット・達成することができました。

まとめ

詳細について話せていない部分もあるかなと思いますが、結果で述べたように、上記のことを整理・効率化することで、 開発状況の差分はあれど半分近くまで減らすことができました。

地道な作業となりますが、今回まとめた観点での改善を行なっていくことで、着実に削減していけると思います。 また、このような取り組みをやっていると普段あまり触らない CI 周りの知識がつくことで、 他の CI 改善にも意識が向くようになり、開発生産性にも寄与する取り組みにつながっていく部分もあると思います。 自分のチームの CI の戦略と照らし合わせながら、ぜひ皆様もこのような改善に取り組んでみてください。

最後まで読んでいただきありがとうございます。ご参考になれば幸いです。