ハコヅミを作る #17:CI の待ち時間と GitHub Actions の無料枠を見直し、Issue をボードで管理する

機能が増えるにつれて、E2E テストも増えてきました。気がつくと、PR を出してから CI が終わるまで10分以上待つようになっていました。

今回は、アプリの機能ではなく、開発の流れの見直しです。

  • デプロイのときは E2E を流さない(#70)
  • PR の E2E を3つに分けて、同時に流す(#88)
  • 増えてきた Issue を、GitHub Projects のボードで管理する(#89)

時間のほとんどは E2E だった

「テストの時間が結構かかる。見直すべきところはある?」と Claude Code に聞いて、最近の CI の実行時間を調べてもらいました。

段階 時間
準備・型チェック・Lint・ユニットテスト 約1分
Playwright のブラウザを入れる 約40秒
E2E テスト 約9分
(デプロイのときだけ)ビルド・マイグレーション・デプロイ 約45秒

さらに、1つの機能を本番まで出すまでに、E2E を4回流していました。

  1. 機能の PR(develop 向け)
  2. develop へのマージで、開発環境にデプロイするとき
  3. リリースの PR(develop → main)
  4. main へのマージで、本番にデプロイするとき

デプロイのワークフローが、PR と同じテストをそのまま呼んでいたためです。

無料枠も危なかった

もう1つ気になったのが、GitHub Actions の無料枠です。ハコヅミのリポジトリは非公開なので、無料で使えるのは月 2,000 分です。

これまでの実行記録(ジョブ 114 件)から計算してもらうと、こうなりました。GitHub はジョブごとに1分単位で切り上げて数えます。

日 使った分数
9月29日(初期構築・MVP) 約150分
9月30日(UI・画像・おすすめ度など) 約270分

1日270分のペースが続くと、7〜8日で2,000分を使い切ります。1つの機能を本番まで出すのに、約40分(10分 × 4回)かかっている計算です。

初期の集中した開発が落ち着けばペースは下がるはずですが、待ち時間も含めて、見直す価値はありそうです。


デプロイのときは E2E を流さない(#70)

PR の CI は、GitHub が作る「PR をマージしたあとの形」でテストしています。ということは、マージした直後のデプロイでもう一度 E2E を流しても、結果はたいてい同じです。

そこで、デプロイの前のテストは、型チェック・Lint・ユニットテストだけ(約1分)にしました。E2E は PR の CI で流します。

  • デプロイは約10分 → 約2分
  • 本番までの E2E は、4回 → 2回(機能の PR と、リリースの PR)

テストの手順を2か所に書かないよう、ci.yml に「E2E を流すか」の入力を足し、デプロイからは e2e: false で呼ぶ形にしています。

組み合わせは試されていない、という穴

この形には穴が1つあります。PR を2つ並行して開いていて、片方をマージしたあとに、もう片方を(CI を流し直さずに)マージすると、その組み合わせは E2E で試されていません。

今は1つずつ順に進めているので、ほとんど起きません。それでも念のため、CLAUDE.md に次のルールを足しました。

PR の CI が通ったあとに別の PR を同じブランチにマージしたら、CI を流し直してからマージする

最後はリリースの PR の CI で、全部そろった形の E2E が流れるので、本番に出る前にもう一度確かめられます。


PR の E2E を3つに分ける(#88)

デプロイは速くなりましたが、PR の CI はそのままです。しかも、消耗品の Issue(#16)で E2E がさらに増え、ついに CI が15分の時間の上限で止められるようになりました。いったん上限を25分に上げてしのぎ、E2E を分けて同時に流すことにしました。

E2E は、ローカルの D1 が同時の書き込みで SQLITE_BUSY になるのを避けるため、1本ずつ順に流しています(#6)。ただ、別々のマシンで流せば、D1 もマシンごとに分かれるので、ぶつかりません。Playwright の --shard で3つに分け、GitHub Actions の matrix で3台同時に流します。

e2e:
  if: github.event_name == 'pull_request' || inputs.e2e
  name: e2e (${{ matrix.shard }}/3)
  strategy:
    fail-fast: false
    matrix:
      shard: [1, 2, 3]
  steps:
    # ...
    - run: npx playwright test --shard=${{ matrix.shard }}/3
  • fail-fast: false:1つが落ちても、残りは最後まで流す(どれが落ちたかをまとめて見られる)
  • 失敗したときのスクリーンショットなどは、playwright-test-results-1 のように分割ごとに名前を分けて残す
  • 3台それぞれでブラウザを入れ直す時間を減らすため、Playwright のブラウザをキャッシュする

!= false と書いてはいけない

最初は、E2E を流す条件を「inputs.e2e が false でなければ」と書こうとしていました。デプロイからは e2e: false で呼ぶので、それ以外(PR のとき)は流れるはず、という考えです。

ところが、PR のとき(pull_request で動いたとき)は、inputs.e2e は**値なし(null)**になります。GitHub Actions の式では、型の違う値を比べるときに数値に直してから比べるので、null も false も 0 になり、null == false が成り立ちます。つまり inputs.e2e != false は PR のときに偽になり、E2E が流れなくなってしまいます。

そこで、「PR のときは必ず流す。呼ばれたときは e2e の指定に従う」と、2つの場合を分けて書いています(上の if:)。ワークフローのコメントにも理由を残しました。

わざと落として確かめた

3つに分けたあと、「1つでも落ちたら CI が失敗になる」ことを確かめるため、中身が expect(1).toBe(2) だけのテストを1件入れて push しました。

  • 落ちるテストが入った e2e (1/3) だけが失敗し、CI 全体も失敗になる
  • e2e (2/3)・e2e (3/3) は止められず、最後まで流れて通る
  • 失敗の記録が playwright-test-results-1 として残る

の3つを確かめてから、そのコミットを取り消しました。#2で CI を作ったときと同じく、「赤くなるところを一度見ておく」は、安心感が違います。

7分の予定が8分だった

結果はこうなりました。

ジョブ 件数 時間
check(型チェック・Lint・ユニットテスト) — 約1分
e2e (1/3) 42件 約8分
e2e (2/3) 41件 約3.5分
e2e (3/3) 41件 約5分
全体 124件 約8分

13〜15分から、ほぼ半分になりました。ただ、AC に書いた「7分前後」には届いていません。

原因は分け方の偏りです。Playwright は最初、ファイル単位で3つに分けるので、時間のかかる画像やキャンプまわりのテストが 1/3 に集まっていました。テスト単位で分ける設定(fullyParallel: true)に変えてみましたが、テストは名前の順に区切られるので、偏りは残りました。

選択肢は3つありました。

  1. このまま受け入れる(AC を「8分前後」に直す)
  2. 4つに分ける(6分前後の見込み。ただし Actions の使用量がさらに増える)
  3. 時間のかかるテストのファイル名を変えて、3つに散らす

半分になって十分効いているので、1 にしました。ファイル名で並び順を調整するのは、あとから見て意味が分かりにくくなります。テストがさらに増えて気になったら、そのとき4つに分けます。

無料枠とのトレードオフ

3つに分けると、待ち時間は短くなりますが、Actions の使用量は増えます。3台それぞれで準備(チェックアウト、npm、ブラウザ、ビルド)をするためで、1回あたり3〜4割ほど増える見込みです。

デプロイで E2E を流さなくした分(1機能あたり約40分 → 約24分)で、その増加を吸収した形です。それでも足りない月が出たら、Actions の「支出の上限」を少しだけ上げるつもりです。上限が0ドルのままなら、請求はされず、Actions が止まるだけです。


Issue をボードで管理する(#89)

もう1つの見直しは、Issue の管理です。思いついたことを /issue でどんどん Issue にしていたら、開いている Issue が10件を超え、どれから手をつけるかが分かりにくくなってきました。

GitHub の Issue には、優先度を入れる専用の欄がありません。ラベルで付ける方法もありますが、並べ替えができません。そこで、GitHub Projects でボードを1つ作りました。非公開のリポジトリでも無料で使えます。

欄 選択肢 決める人
優先度 高・中・低 人(Claude Code は案を出す)
Status 未着手・作業中・受入待ち・本番待ち・完了 Claude Code が流れに合わせて動かす

「本番待ち」は、ハコヅミの流れに合わせて足した状態です。開発環境で受入確認は済んだけれど、本番の D1 にテーブルができたことなど、本番でしか確かめられない AC が残っている Issue を表します。これまでは「開いているけれど、何を待っているのか」が Issue を開くまで分かりませんでした。

スキルに組み込む

ボードを作っただけでは、更新を忘れます。そこで、Claude Code のスキル(#4)と CLAUDE.md に、ボードの動かし方を書き足しました。

  • /issue で Issue を作ったら、ボードに「未着手」で入れ、優先度も入れる
  • /implement で取りかかったら「作業中」
  • PR がマージされたら「受入待ち」
  • 受入 OK で本番の AC が残っていれば「本番待ち」、Issue を閉じたら「完了」

gh project item-edit で欄を変えるには、欄と選択肢の ID が要ります。ID は CLAUDE.md に書いておき、Claude Code が毎回調べ直さなくてよいようにしました。

つまずいたことも2つありました。

  • ボードの一覧は、足した直後に反映されないことがある。足したはずの Issue が一覧に出てこず、足し直していました。確かめるときは、しばらくおいてから見る
  • 手元のシェルは zsh。複数の Issue をまとめて動かすループで、bash のつもりで書いた単語の分け方が効かなかった。まとめて動かすときは bash のスクリプトにする

どちらも CLAUDE.md に書いておき、次から同じところでつまずかないようにしています。

なお、ボードの見え方(優先度で並べる、Status の列で並べるボード表示など)は API では作れないので、ブラウザで設定しました。


おわりに

  • 時間のほとんどは E2E。デプロイでは流さず、PR の CI で流す(デプロイは約2分に)
  • PR の E2E は3つに分けて同時に流す(13〜15分 → 約8分)。ただし Actions の使用量は増える
  • GitHub Actions の式では null == false。!= false で書くと、PR で E2E が流れない
  • 増えてきた Issue は、GitHub Projects のボードで優先度と状態を管理する。動かし方はスキルに組み込む

ここまでの機能(消耗品、キャンプ場の URL と地図)は、分けた E2E の CI を通って、本番にもリリースしました。次は、優先度「高」にした「まだ積み込んでいない項目を目立たせる」です。

← トップページに戻る