ハコヅミを作る #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回流していました。
- 機能の PR(develop 向け)
- develop へのマージで、開発環境にデプロイするとき
- リリースの PR(develop → main)
- 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つありました。
- このまま受け入れる(AC を「8分前後」に直す)
- 4つに分ける(6分前後の見込み。ただし Actions の使用量がさらに増える)
- 時間のかかるテストのファイル名を変えて、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 を通って、本番にもリリースしました。次は、優先度「高」にした「まだ積み込んでいない項目を目立たせる」です。