はじめに
Java アプリケーションのCIでのテスト時間短縮が長いので改善した話。
対象のプロジェクトでは、GtiHub の1つのリポジトリで10以上のJavaアプリケーションを管理している。
コミットをプッシュしたら、全アプリケーションの全単体テストがテストされるのだが、これがだいたい20分〜30分ほど掛かっていて、結構辛い。
サービスが成長してアプリケーションが増えていくと、さらに時間がかかりそうだったので、手を打つことにした。
やったこと
- テストの並列化
- 変更したアプリケーションだけテスト
対象のプロジェクト
複数の Java アプリケーションを Gradle のマルチプロジェクト構成でまとめている。
イメージはこんな感じ
hoge-project
├── applicationA
│ └── src
│ ├── main
│ └── test
├── applicationB
│ └── src
│ ├── main
│ └── test
├── ...
├── ...
├── ...
├── build.gradle.kts
└── settings.gradle.kts
GtiHub に feature ブランチをプッシュすると、GitHub Actions のワークフローで全アプリケーションの全テストを実行している。
具体的には gradle build
を実行していて、その中で単体テストや ktlint が実行されていて、エラーがあればワークフローが失敗する。
GitHub Actions のワークフローがこちら
name: test
on:
push:
branches-ignore:
- main
jobs:
test:
name: test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup Java
uses: actions/setup-java@v3
with:
...
- name: Test
run: ./gradlew build
ワークフローが失敗するとその feature ブランチは main ブランチにマージできないように制限されている。制限は、GitHub の Branch protection rule
でやっている。
改善
テストの並列化
GitHub Actions の matrix strategy
を使って各アプリケーションのテストを並列で実行することにした。
これで最もテストに時間にかかるアプリケーションに律動してCIが終わるようになったので、全体のビルド時間がかなり短縮された。
長いもので5分〜10分ほどかかっていたので、だいたいその時間でCIが終わるようになった。
GitHub Action のワークフローはこうなる
...
jobs:
setup-matrix:
name: setup-matrix
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup matrix
id: setup-matrix
run: |
apps=$(cat "apps.csv")
echo ::set-output name=applications::[$apps]
outputs:
apps: ${{ steps.setup-matrix.outputs.apps }}
test:
name: test
needs: setup-matrix
runs-on: ubuntu-latest
strategy:
max-parallel: 20
matrix:
app: ${{ needs.setup-matrix.outputs.apps }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup java runtime
uses: actions/setup-java@v3
with:
...
- name: Build
run: ./gradlew :${{ matrix.app }}:build
all_test:
name: all_test
needs: [test]
runs-on: ubuntu-latest
steps:
- name: Finish
run: echo "done"
apps.csvは、アプリケーションの名前の一覧が入ったcsvファイル
applicationA, applicationB, ...
GitHub の Branch protection rule
> Require branches to be up to date before merging
には all_test
を選択しておく。これで全部のテストがとおらないと main ブランチにマージできないようになる。
変更したアプリケーションだけテスト
変更したアプリケーションだけテストすることにした。
もちろん共通ライブラリに手を入れたときは、全アプリケーションのテストが必要なので、そのあたりのチェックが必要になる。
全アプリケーションに影響するような変更は稀なので、この改善で、小さな変更を頻繁にリリースするときのリードタイムが改善された。
小さなアプリケーションなら3分未満でテストが終わるので、いままで20〜30分待っていたことを考えると、かなりうれしい。
差分チェックするシェルスクリプトを作った。
IGNORES=".github,docs,README.md,.gitignore,settings-modules.github"
ALL_APPS_FILE="apps.csv"
if [ ! -f $APPS_FILE ]; then
echo "ERR! $APPS_FILE not found" >&2
exit 1
fi
ALL_APPS=$(cat $ALL_APPS_FILE)
## 差分があるトップレベルのファイルとディレクトリを取得
git fetch --depth 1 origin main
DIFF="$(git diff origin/main HEAD --name-only | awk -F'/' '{print $1}' | uniq)"
## 差分から無視リストを除外
for ignore in ${IGNORES//,/ } ; do
DIFF="$(echo "$DIFF" | sed "s/^${ignore}\$//g")"
done
## 変更されたモジュールをビルド対象にする
APPS=''
for app in ${ALL_APPS} ; do
d="$(echo "$DIFF" | sed "s/^${app}\$//g")"
if [ "$d" != "$DIFF" ]; then
APPS=${APPS}${module}`,`
fi
DIFF="$d"
done
## モジュール以外のファイルが変更されていたらすべてビルドする
if [ "$(echo $DIFF | tr -d '\n')" != "" ]; then
APPS=$ALL_APPS
fi
echo "$APPS"
🔥 このシェルスクリプトは、実際のものから多少変更しているので動かないかもしれません。 またエラーアンドリングも甘いし可読性も低いので書き直したい気持ちもあります。 あくまで参考までに載せます。
上記のシェルスクリプトを diff.sh という名前で保存しておく。
GitHub Action のワークフローはこうなる
...
jobs:
setup-matrix:
name: setup-matrix
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup matrix
id: setup-matrix
run: |
apps=$(bash ./diff.sh)
echo "::set-output name=modules::[$apps]"
outputs:
apps: ${{ steps.setup-matrix.outputs.apps }}
test:
name: test
needs: setup-matrix
runs-on: ubuntu-latest
if: ${{ needs.setup-matrix.outputs.apps != '[]' }}
strategy:
max-parallel: 20
matrix:
app: ${{ needs.setup-matrix.outputs.apps }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup java runtime
uses: actions/setup-java@v3
with:
...
- name: Build
run: ./gradlew :${{ matrix.app }}:build
all_test:
name: all_test
needs: [test]
runs-on: ubuntu-latest
steps:
- name: Finish
run: echo "done"
まとめ
かなり改善できたのでは 🎉
テストの並列化で、最もテストに時間にかかるアプリケーションに律動してCIが終わるようになったので、全体のビルド時間がかなり短縮された。
変更したアプリケーションだけテストすることで、小さな変更を頻繁にリリースするときのリードタイムが改善された。
ソースコードを触った後、テストやビルド、リリース作業に時間を取られてしまうのは本質的ではないので、まだまだ改善して快適なCI/CD環境にしていきたい。