【Day 3】CI はプロジェクト初期に組むと幸せになれるはず【じゃんけんアドカレ】

[じゃんけんアドベントカレンダー](https://qiita.com/advent-calendar/2020/janken) の 3 日目です。

- 初回 ... [【Day 1】とりあえず 1 クラスに全部書く【じゃんけんアドカレ】](https://qiita.com/os1ma/items/630e2d7fb5d744bbdf8a)
- 前回 ... [【Day 2】リファクタリングするなら自動テスト【じゃんけんアドカレ】](https://qiita.com/os1ma/items/7ee7d1f43e3e50d8e23d)

---

[昨日](https://qiita.com/os1ma/items/7ee7d1f43e3e50d8e23d) は自動テストを作成しましたが、自動テストは作っただけだと実行されなかったりします。
そこで今日は**自動テストが間違いなく実行されるよう、CI を構築**したいと思います。

アジャイル開発などで教科書的には最初に用意しろと言われているにも関わらず、時間がないからと後回しになりがちな CI ですが、今回はちゃんとプロジェクト初期に構築します。

今回構築する CI では、テストとビルドを実行するに止まらず、**依存関係の脆弱性診断も組み込もうと思います**。
いわゆる「DevSecOps」を意識しています。

## CI ツールはどうするか

CI を構築するにあたり、ツールをどうするかという話があります。
今回はどのプラットフォームにデプロイするのか未定なので、AWS CodeBuild などのデプロイ先の環境が提供する CI サービスではなく、CI 専用の SaaS を使うことを考えてみます。
その場合、現状では **CircleCI か GitHub Actions** が第一候補です。

CircleCI と GitHub Actions のどちらを使うかは好みで決めて大丈夫ですが、個人的には GitHub Actions の方が好きです。
というのも、**GitHub Actions は CircleCI と異なり、GUI 上の操作不要で、GitHub に設定ファイルを push するだけで設定できます**。[^1]
[^1]: ちなみに GitLab CI でも同様のことができます
これは CircleCI にはないメリットです。

そこで今回は GitHub Actions を使おうと思います。

ちなみに、CI サービスの選定の考え方については自分の個人ブログの [この記事](https://www.kanzennirikaisita.com/posts/ci-services) にまとめていたりするので、興味がある方は是非見てみてください。

## GitHub Actions のセットアップ

さて、それでは GitHub Actions をセットアップします。

といってもセットアップは非常に簡単で、**.github/workflows ディレクトリに YAML ファイルをコミットして push するだけ**です。

### YAML ファイルの中身

今回は以下の YAML ファイルを用意しました。
(GitHub の [こちら](https://github.com/os1ma/JankenEnterpriseEdition/blob/2fe00d184da8f55e46fa0dbbc0bc8c9684b1dcd1/.github/workflows/workflow.yaml) からも確認可能です)

```yaml
name: workflow

on:
  push:
    branches:
    - main
  pull_request:
    branches:
    - main
  schedule:
    # 日本時間で毎晩 21 時
    - cron: '0 12 * * *'

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - uses: actions/cache@v2
      with:
        path: ~/.gradle/caches
        key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
        restore-keys: ${{ runner.os }}-gradle
    - uses: actions/setup-java@v1
      with:
        java-version: '11.0.9'
        java-package: jdk
        architecture: x64
    - run: ./bin/build.sh
    - name: Archive reports
      uses: actions/upload-artifact@v2
      if: ${{ always() }}
      with:
        name: reports
        path: ./app/build/reports/**
    - name: Archive JAR
      uses: actions/upload-artifact@v2
      with:
        name: jar
        path: ./app/build/libs/app.jar
        if-no-files-found: error
```

簡単に言えば、

- main ブランチへの push やプルリクエスト、または定期実行をトリガーとして
- ビルド用のシェルスクリプトを実行し
- 成果物を保存する

だけです。

では、いくつか特筆すべき点だけ説明していきます。

### キャッシュ

**SaaS の CI サービスでは実行環境が毎回リセットされるため、キャッシュを特に設定していないとビルドが毎回依存関係のダウンロードから始まり、実行時間が長くなってしまう場合があります**。

**Java (Gradle) であれば ~/.gradle、Node.js であれば node_modules といったディレクトリをキャッシュに保存しておくことで、依存関係のダウンロード時間を削減できることが多いです**。

GitHub Actions の場合は [cache@v2](https://github.com/actions/cache) というアクションを使うことで実現可能です。

```yaml
    - uses: actions/cache@v2
      with:
        path: ~/.gradle/caches
        key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
        restore-keys: ${{ runner.os }}-gradle
```

CircleCI や AWS CodeBuild など、どんな CI サービスにも同様の機能があります。

### 成果物の保存

以下の記述により、CI で作成された成果物を保存しています。

```yaml
    - name: Archive reports
      uses: actions/upload-artifact@v2
      if: ${{ always() }}
      with:
        name: reports
        path: ./app/build/reports/**
    - name: Archive JAR
      uses: actions/upload-artifact@v2
      with:
        name: jar
        path: ./app/build/libs/app.jar
        if-no-files-found: error
```

./app/build/reports/** で指定しているテスト結果などのレポートについては、`if: ${{ always() }}` という記述により、ビルドエラーが発生した際でも保存されるようにしています。

JAR ファイルについては、`if-no-files-found: error` という設定により、存在しなければジョブをエラーにするよう設定しています。

ちなみにですが、成果物のデフォルトの保存期間は 90 日です。
その他の設定など、詳しくは Upload-Artifact v2 の [GitHub リポジトリ](https://github.com/actions/upload-artifact) を参照ください。

このように、他の CI サービスでは YAML の記法として組み込まれている cache や artifact といった設定がプラグイン的になっているのは、GitHub Actions の面白いところだと思います。

## pre-commit の設定

さて、CI の基本的なセットアップはもう完了してしまいましたが、CI で実行している処理はローカルでもコミット前に確認されると嬉しかったりしないでしょうか。

よくある手法として、**Git の pre-commit のフックを利用して、コミット時にビルドが通るかチェックする**というものがあります。

実際に、以下のコマンドをプロジェクトホームで実行することで ./bin/build.sh を実行する pre-commit のフックを作成できます。[^2]

[^2]: printf の最後に \n を入れているのは、POSIX の仕様で最終行に改行を入れる、というルールに従うためです

```bash
printf '#!/bin/bash\n'"$(pwd)"'/bin/build.sh\n' > .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
```

上記のコマンドの通り、**pre-commit の設定は .git 以下に保存されるため、git clone のたびに各自が毎回設定する必要があります**。

pre-commit を設定すれば CI は不要ではないかと思われるかもしれませんが、以下の 3 つの理由から **CI はやはり必要**です。

1. pre-commit は強制できないこと
2. git add していないファイルがある場合、pre-commit ではビルドが通っても、コミット済みのファイルだけでビルドが通るとは限らないこと
3. **ローカルでビルドできるからといって他の環境でもビルドできる保証はない**こと

CI の環境を作るのが何らかの理由で難しいなど、特殊な事情がある場合はまずは pre-commit だけでも設定しておくといいかもしれません。

## OWASP Dependency Check

続いて、Java の依存関係の脆弱性診断を入れてしまおうと思います。

DevSecOps 的には、依存関係の脆弱性診断だけでなく、アプリケーションレベルでの脆弱性診断やインフラの脆弱性診断なども CI 組み込むべきかもしれません。
とはいえそこまで一気に対応するのは大変なので、今回まずは依存関係の脆弱性診断だけを組み込みます。

Java の依存関係の脆弱性診断のツールとしては、[OWASP Dependency Check](https://jeremylong.github.io/DependencyCheck/) を使います。

導入は簡単で、build.gradle にプラグインを追加するだけです。

```build.gradle
plugins {
    :
    id 'org.owasp.dependencycheck' version '6.0.3'
}
```

この設定だけで、

```shell
./gradlew dependencyCheckAnalyze
```

のコマンドで脆弱性診断が実行され、結果がレポートとして保存されるようになります。

さらに、例えば、build.gradle に以下のような記述を入れれば、CVSS 7.0 以上の脆弱性が発見された場合にビルドエラーになります。[^3]

[^3]: CVSS はこれだけを参照すれば良い万能のスコアではないと言われているのでご注意ください

```build.gradle
dependencyCheck {
    failBuildOnCVSS = 7.0F
}
```

このように依存関係の脆弱性診断を組み込み場合は、時間の経過によって新たな脆弱性が生じていないか確認するため、CI は push やプルリクエストをトリガーに実行するだけでなく、日時などで定期実行するようにしておくと良いでしょう。

なお、OWASP Dependency Check は実行時間が結構長くなりやすいので、pre-commit で実行するかは要検討です。

## バッジの追加

CI を一通り構築できたので、最後に README に GitHub Actions のステータスのバッジをつけようと思います。

README に以下のように書くだけです。

```markdown
![badge.svg](https://github.com/os1ma/JankenEnterpriseEdition/workflows/workflow/badge.svg)
```

※ URL は適宜変更が必要です。

バッジは https://github.com 以下で提供されているため、GitHub と同じ認証情報でアクセスできるようで、private リポジトリであってもトークンなどの設定は不要です。
CircleCI のバッジを private リポジトリにつけるときはトークンの設定が必要だったりするので、ここも GitHub Actions ならではのメリットかもしれません。

## 次回のテーマ

前回・今回で自動テストと CI をセットアップし、ついにリファクタリングの準備が整いました。
次回からついにコードのリファクタリングを進めていきます。

まずは典型的な Web フレームワークを使った際の MVC のような構成を CLI アプリケーションのまま目指していきます。

それでは、今回の記事はここまでにします。最後まで読んでくださりありがとうございました。
[じゃんけんアドベントカレンダー](https://qiita.com/advent-calendar/2020/janken) に興味を持ってくださった方は、是非購読お願いします。

## 次回の記事

[【Day 4】DTO と Enum を使ってみる【じゃんけんアドカレ】](https://qiita.com/os1ma/items/56397861db93cd20bc6c)

## 現時点のコード

現時点のコードは GitHub の [この時点のコミット](https://github.com/os1ma/JankenEnterpriseEdition/tree/2fe00d184da8f55e46fa0dbbc0bc8c9684b1dcd1) を参照ください。