2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AI駆動開発で実装が速くなったら、CI待ちが一番長くなった——品質を落とさずCIを軽くする

2
Posted at

はじめに

AIにコードを書いてもらっていると、大抵1時間以内にアウトプットがあがってきます。
変更を頼んで、出てきたコードを確認して、テストを動かす。
このサイクルが以前より早くなりました。

その一方で、気になってきたのがCIの待ち時間です。修正はできたのに、チェックが終わらない。エラーが出たら直して、また待つ。実装が速くなった分、この時間が目につきやすくなりました。

もちろん、だからといってテストを減らせばいい、という話でもありませんよね。必要な確認を残しながら、少しでも早く結果を受け取れないか。この記事では、そんな視点でCIの見直し方を考えてみます。

GitHub ActionsでPRのチェックを運用していて、CIの待ち時間が気になっている方向けの記事です。

この記事は、CI待ちについて考えたことを整理したものです。以下のエピソード・時間・構成は架空の例で、CIを改善した実測結果ではありません。タイトルの「一番長くなった」も問題提起としての表現です。

参考資料

:hourglass_flowing_sand: 修正は終わった。でも、次に進めない

こんな場面を想像してみてください。AIに小さな修正を頼むと、ほどなくコードができました。差分を確認してPRを更新し、あとはCIの結果を待つだけです。

しばらくして、単体テストの失敗が1件。原因を見てAIに修正を頼むと、またすぐに差分が出てきます。PRを更新して、もう一度CIへ。今度は通るはず……と思いながら、画面の進行状況を見ています。

この場面は、問題を説明するために作った架空の例です。でも、実装よりも確認結果を待つ時間が気になる感覚には、心当たりがある人もいるのではないでしょうか。

自分の自動実行ワークフローでは、処理そのものより環境準備が実行時間の多くを占めることもありました。これは定期ジョブでの経験で、CIを改善した実績ではありません。ただ、「遅いのはテストのせい」と決めつける前に、何を待っているのか見たほうがよさそうだと感じています。

:watch: まず「何を待っているのか」を分ける

「CIが遅い」とひとくくりにしても、待っている場所はさまざまです。まずは次のように分けて見てみます。

見る場所 時間の例 改善の方向
キュー待ち ランナーが空くまで 同時実行数や不要な実行を確認
環境準備 checkout、言語のセットアップ、依存関係の取得 ダウンロードやキャッシュを確認
チェック実行 lint、単体テスト、結合テストなど 並列化や重複の解消を検討
ジョブ間の待ち 前のジョブの完了待ち 本当に依存しているか確認

GitHub Actionsの実行画面で、まずワークフロー全体の経過時間と各ジョブ・ステップの所要時間を見ます。キュー待ちと実行時間は別です。テストそのものが速くても、ランナー待ちが長ければ、テストコードの見直しだけでは解決しません。

ここで気をつけたいのが、1回だけの結果で判断しないことです。初回のキャッシュ作成や、そのときの混雑でも時間は変わります。変更前後それぞれで複数回の実行を見て、対象ブランチ、変更内容、キャッシュのヒット状況、ランナーの種類を揃えられる範囲で記録します。中央値と、遅いときの値の両方を見ると、普段の体感とばらつきを把握しやすくなります。

:runner: PRで欲しいのは、まず早いフィードバック

最初に知りたいのは、たとえば構文エラーや単体テストの失敗ですよね。時間のかかる結合テストが終わるまで待たなくても、明らかな問題が分かれば修正を始められます。

そこで試したいのが、役割の違うチェックを別のジョブに分ける方法です。

PR作成・更新
  ├─ 静的解析・単体テスト ─── 早いフィードバック
  └─ 結合テスト ──────────── 必須の品質確認

両方の必須チェックが成功 → マージ可能

:construction: ここで誤解されたくないのですが、早いチェックが終わったら、残りを待たずにマージするという話ではありません。独立して実行できる処理を並べれば、片方の失敗を早く知ることと、両方の成功をマージ条件にすることを両立できます。

:twisted_rightwards_arrows: 例:独立したジョブを並列にする

次はPythonプロジェクトを想定した、GitHub Actionsの例です。テストのディレクトリや依存関係ファイルは仮のものなので、そのまま自分のプロジェクトに存在するか確認して置き換えてください。

name: CI

on:
  pull_request:

permissions:
  contents: read

jobs:
  unit:
    name: unit
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
          cache: pip
          cache-dependency-path: requirements-dev.txt
      - run: python -m pip install -r requirements-dev.txt
      - run: python -m pytest tests/unit

  integration:
    name: integration
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
          cache: pip
          cache-dependency-path: requirements-dev.txt
      - run: python -m pip install -r requirements-dev.txt
      - run: python -m pytest tests/integration

この2つのジョブには needs を指定していないため、互いの完了を待つ依存関係はありません。GitHub Actionsは独立したジョブを並列に実行できますが、実際に同時実行されるかは、利用できるランナーや同時実行の上限に左右されます。

このYAMLを置くだけでマージがブロックされるわけではありません。必要であればリポジトリのブランチ保護やルールセットで unit と integration の両方を必須チェックに設定します。ジョブ名を変更した場合は、必須チェックの設定との対応も見直します。

requirements-dev.txt には pytest など、各テストが必要とする依存関係が含まれている前提です。結合テストにDBなどの外部サービスが必要なら、サービスの準備とテストデータの分離も別途必要です。サンプルには記載していません。

並列化の効果は、常に同じではない

仮に、単体テストが3分、結合テストが8分で、それぞれ準備が終わった後に同時に動くなら、両方の完了を待つ時間は概ね長いほうに近づきます。ただし実際には、2つのジョブにそれぞれ環境準備が必要で、キュー待ちやキャッシュの状態も異なります。

ただ、並列にすれば必ず半分になる、というほど単純でもありません。ジョブを細かく分けすぎると、準備の重複やランナーの競合が増えることもあります。まず実行時間を測り、分けた後も比較するのが確実です。

:package: キャッシュは「環境の再利用」ではなく「準備の一部を省く」

先ほどの例では、setup-python の cache: pip を使っています。これはpipがダウンロードするパッケージなどを再利用し、依存関係の取得を速くするための設定です。インストール済みの実行環境を丸ごと保存するものではありません。 pip install 自体は毎回実行します。

依存関係が変わった場合には適切な内容を取得し直せるよう、依存関係ファイルをキャッシュキーに反映させます。キャッシュがない初回や、依存関係を更新した直後はヒットしないこともあります。ヒットした場合としない場合の両方でジョブが成功することを確認しておくと、キャッシュの有無でテストの成立が変わる事故を避けやすくなります。

キャッシュには認証情報や機密データを入れません。特に外部からのPRがあるリポジトリでは、キャッシュを読み書きできる範囲を公式ドキュメントで確認します。

:calendar: 重いテストを定期実行に回す前に

重いテストは夜間に回せばいいのでは? と考えたくなりますよね。たしかにPRは軽くできます。ただし、マージ前に検知すべき障害を、マージ後まで見逃してよいかを先に決める必要があります。

たとえば、主要な機能間の連携やデータ整合性を守るテストがリリース前に必須なら、定期実行に移しただけでは代わりになりません。PRでは代表ケースを確認し、より広い組み合わせを定期実行する方法もありますが、代表ケースで守れる範囲と、守れなくなる範囲をチームで確認します。

定期実行に分ける場合も、次を曖昧にしないようにします。

  • 失敗を誰が、どこで確認するか
  • 失敗中に新しい変更をマージしてよいか
  • 失敗したとき、どの変更から原因を調べるか
  • リリース前に全体の検証を再実行する必要があるか

せっかくCIを短くしても、必要な検証を外した結果、あとから障害対応に追われては本末転倒です。このあたりは、速くしたい気持ちと慎重に確認したい気持ちの両方があります。

変更ファイルによるスキップは慎重に

「ドキュメントだけの変更ならテストを走らせない」「バックエンドだけを変更したらフロントエンドのテストを省く」という考え方もあります。しかし、共有設定や共通ライブラリの変更が複数の領域に影響する場合、対象を狭くしすぎると問題を見逃します。

GitHub Actionsでは、ワークフローを paths などの条件でスキップした場合、関連するチェックが Pending のままになり、必須チェックとして設定されているとマージを妨げることがあります。まずは対象範囲とブランチ保護の動きを検証環境で確認し、必須チェックがどう報告されるかまで確かめます。

高速化の効果が測れていない段階では、いきなりテスト対象を絞るより、環境準備のキャッシュや、独立したジョブの並列化から調べるほうが影響範囲を判断しやすいと考えています。

改善後に見る数字

CIを見直したあと、何をもって「速くなった」と言うかも悩ましいところです。自分なら、次の2つを分けて見たいです。

  1. 最初に役立つ結果が返るまでの時間:たとえば単体テストの失敗を知るまで。
  2. マージに必要なチェックがすべて終わるまでの時間:必要な品質確認が揃うまで。

:thought_balloon: 前者だけが短くなり、後者は変わらないこともあります。それでも早く失敗を知れれば、長いジョブが動いている間に次の修正へ移れます。一方で、結果が返るのが速くなっても、マージに必要なチェックが意図せず省略されていないかは別に確認します。

変更前後でテスト件数や対象、必須チェックの設定も記録します。単純に時間が短くなっていても、テストが走らなくなった結果なら、目指した改善ではありません。

おわりに

AIと開発していると、コードを書く速さについ目が向きます。でも、修正した結果が返ってくるまで待っている時間も、開発の一部なんですよね。

自分も、まずは「どこで待っているのか」を見て、必要なテストを残したまま早く結果を返せるところから考えたいと思います。みなさんのCIでは、何に一番時間がかかっていますか?

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?