Docker X Laravel GitHub Actions でCIを回してDockerとLaravelの自動テストを実行する

DockerfileやLaravelプロジェクトのコードを変更された時に毎度手動でテストするのはとても時間がかかってしまいます。
Dockerビルド以外にも、Composerのインストールがうまくいくか、マイグレーションやロールバック処理やシーダーが壊れてないか。
Laravelのテストコードがうまく実行されるか。確認したいことがたくさんあります。

## 環境

- https://github.com/ucan-lab/docker-laravel
  - 参考リポジトリ: https://github.com/ucan-lab/docker-laravel-github-actions

## パターン1: まとめてテストする

https://github.com/ucan-lab/docker-laravel-github-actions/pull/2

```yml:.github/workflows/integration-laravel-testing.yml
name: Laravel Testing

on:
  pull_request:

jobs:
  laravel-testing:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2

      - name: Ubuntu Version
        run: cat /etc/os-release

      - name: Docker Version
        run: docker version

      - name: Cache Composer dependencies
        uses: actions/cache@v2
        id: cache
        with:
          path: ./backend/vendor
          key: ${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
          restore-keys: ${{ runner.os }}-composer-

      - name: Docker Compose Build and Up
        run: |
          docker-compose build
          docker-compose up -d

      - name: Docker Compose Process
        run: docker-compose ps -a

      - name: PHP, Composer Version
        run: |
          docker-compose exec -T app php --version
          docker-compose exec -T app composer --version

      - name: Composer Install
        if: steps.cache.outputs.cache-hit != 'true'
        run: docker-compose exec -T app composer install -q --no-ansi --no-interaction --no-scripts --no-suggest --no-progress --prefer-dist

      - name: Composer Dump Autoload
        run: docker-compose exec -T app composer dump-autoload -q

      - name: Laravel Settings
        run: |
          docker-compose exec -T app cp .env.example .env
          docker-compose exec -T app php artisan key:generate
          docker-compose exec -T app php artisan optimize
          docker-compose exec -T app chmod -R 777 storage bootstrap/cache

      - name: Laravel Version
        run: docker-compose exec -T app php artisan --version

      - name: Laravel Migrate Testing
        run: docker-compose exec -T app php artisan migrate

      - name: Laravel Rollback Testing
        run: docker-compose exec -T app php artisan migrate:refresh

      - name: Laravel Seeding Testing
        run: docker-compose exec -T app php artisan db:seed

      - name: Laravel PHPUnit Testing
        run: |
          docker-compose exec -T app php artisan config:clear
          docker-compose exec -T app php artisan test
```

### パターン1の特徴

Laravel Testing: 2m 35s
(この2分13秒はDockerビルドの時間です。)

愚直にDockerビルドしてDockerイメージを作成してコンテナを作成し、そのコンテナ上でLaravelのテストを実行してます。
よくない点としては、毎回Dockerビルドが実行されるので時間がもったいない感じがあります。
(PHPのファイル変更に比べてDockerfileの変更が入る機会が少ないので)

## パターン2: テストを分割する

https://github.com/ucan-lab/docker-laravel-github-actions/pull/3

```yml:.github/workflows/integration-docker-testing.yml
name: Docker Testing

on:
  pull_request:
    paths:
      - 'infra/*'

jobs:
  docker-testing:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2

      - name: Ubuntu Version
        run: cat /etc/os-release

      - name: Docker Version
        run: docker version

      - name: Docker Compose Build
        run: docker-compose build
```

```yml:.github/workflows/integration-laravel-testing.yml
name: Laravel Testing

on:
  pull_request:
    paths:
      - 'backend/*'

jobs:
  laravel-testing:
    runs-on: ubuntu-latest

    services:
      mysql:
        image: mysql:8.0
        ports:
          - 3306:3306
        env:
          MYSQL_ROOT_PASSWORD: secret
          MYSQL_DATABASE: laravel_local
        options: >-
          --health-cmd "mysqladmin ping -h localhost"
          --health-interval 20s
          --health-timeout 10s
          --health-retries 10

    env:
      DB_CONNECTION: mysql
      DB_HOST: 127.0.0.1
      DB_PORT: 3306
      DB_DATABASE: laravel_local
      DB_USERNAME: root
      DB_PASSWORD: secret

    defaults:
      run:
        working-directory: backend

    steps:
      - uses: actions/checkout@v2

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.0'

      - name: Ubuntu Version
        run: cat /etc/os-release

      - name: PHP, Composer Version
        run: |
          php --version
          composer --version

      - name: Cache Composer dependencies
        uses: actions/cache@v2
        id: cache
        with:
          path: ./backend/vendor
          key: ${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
          restore-keys: ${{ runner.os }}-composer-

      - name: Composer Install
        if: steps.cache.outputs.cache-hit != 'true'
        run: composer install -q --no-ansi --no-interaction --no-scripts --no-suggest --no-progress --prefer-dist

      - name: Composer Dump Autoload
        run: composer dump-autoload -q

      - name: Laravel Settings
        run: |
          cp .env.example .env
          php artisan key:generate
          php artisan optimize
          chmod -R 777 storage bootstrap/cache

      - name: Laravel Version
        run: php artisan --version

      - name: Laravel Migrate Testing
        run: php artisan migrate

      - name: Laravel Rollback Testing
        run: php artisan migrate:refresh

      - name: Laravel Seeding Testing
        run: php artisan db:seed

      - name: Laravel PHPUnit Testing
        run: |
          php artisan config:clear
          php artisan test
```

### パターン2の特徴

Laravel Testing: 59s
Docker Testing: 1m 57s

`Laravel Testing` が1分以内に終わっているので、開発効率はとても上がります。
`Docker Testing` はとりあえずDockerビルドが通るかどうかのテストまで行ってます。

`on.<push|pull_request>.paths` で指定したパスに一致するファイルが変更された場合にワークフローが実行されるように設定してます。
`jobs.<job_id>.services` ワークフロー中のジョブのためのサービスコンテナを構築できます。

デメリットとしてはCI上でPHPやMySQLの実行環境を作って実行させているので、Dockerfileの内容と合わせて両方メンテしていく必要があります。

## パターン3: Dockerイメージを登録する

https://github.com/ucan-lab/docker-laravel-github-actions/pull/4

```yml:.github/workflows/integration-docker-image-register.yml
name: Docker Image Register

on:
  pull_request:
    paths:
      - 'infra/*'

jobs:
  docker-image-register:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v1

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v1

      - name: Login to DockerHub
        uses: docker/login-action@v1
        with:
          registry: docker.io
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Build & Push app Container
        uses: docker/build-push-action@v2
        with:
          context: .
          file: ./infra/docker/php/Dockerfile
          push: true
          tags: ucanlab/docker-laravel-github-actions-app:latest

      - name: Build & Push nginx Container
        uses: docker/build-push-action@v2
        with:
          context: .
          file: ./infra/docker/nginx/Dockerfile
          push: true
          tags: ucanlab/docker-laravel-github-actions-web:latest

      - name: Build & Push db Container
        uses: docker/build-push-action@v2
        with:
          context: .
          file: ./infra/docker/mysql/Dockerfile
          push: true
          tags: ucanlab/docker-laravel-github-actions-db:latest
```

```yml:.github/workflows/integration-laravel-testing.yml
name: Laravel Testing

on:
  pull_request:

jobs:
  laravel-testing:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2

      - name: Ubuntu Version
        run: cat /etc/os-release

      - name: Docker Version
        run: docker version

      - name: Cache Composer dependencies
        uses: actions/cache@v2
        id: cache
        with:
          path: ./backend/vendor
          key: ${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
          restore-keys: ${{ runner.os }}-composer-

      - name: Docker Compose Build and Up
        run: |
          docker-compose build
          docker-compose up -d

      - name: Docker Compose Process
        run: docker-compose ps -a

      - name: PHP, Composer Version
        run: |
          docker-compose exec -T app php --version
          docker-compose exec -T app composer --version

      - name: Composer Install
        if: steps.cache.outputs.cache-hit != 'true'
        run: docker-compose exec -T app composer install -q --no-ansi --no-interaction --no-scripts --no-suggest --no-progress --prefer-dist

      - name: Composer Dump Autoload
        run: docker-compose exec -T app composer dump-autoload -q

      - name: Laravel Settings
        run: |
          docker-compose exec -T app cp .env.example .env
          docker-compose exec -T app php artisan key:generate
          docker-compose exec -T app php artisan optimize
          docker-compose exec -T app chmod -R 777 storage bootstrap/cache

      - name: Laravel Version
        run: docker-compose exec -T app php artisan --version

      - name: Laravel Migrate Testing
        run: docker-compose exec -T app php artisan migrate

      - name: Laravel Rollback Testing
        run: docker-compose exec -T app php artisan migrate:refresh

      - name: Laravel Seeding Testing
        run: docker-compose exec -T app php artisan db:seed

      - name: Laravel PHPUnit Testing
        run: |
          docker-compose exec -T app php artisan config:clear
          docker-compose exec -T app php artisan test
```

```yml:docker-compose.yml
services:
  app:
    image: ucanlab/docker-laravel-github-actions-app:latest
    # ...

  web:
    image: ucanlab/docker-laravel-github-actions-web:latest
    # ...

  db:
    image: ucanlab/docker-laravel-github-actions-db:latest
    # ...
```

### パターン3の特徴

Laravel Testing: 1m 0s
Docker Image Register: 2m 40s

DockerイメージをDockerレジストリへ登録します。
今回のレジストリは `docker.io` を使用します。

- https://hub.docker.com/settings/security
  - Docker Hubへのアクセストークンを取得します。
- https://github.com/<オーナー>/<リポジトリ>/settings/secrets/actions
  - GitHub のシークレットへ `DOCKERHUB_USERNAME`, `DOCKERHUB_TOKEN` を登録します。

Dockerレジストリへ登録するためあらかじめDockerアカウントを作ったり、アクセストークンを発行してGitHubシークレットへ登録しておくなど準備が必要です。
タグ指定でバージョンを設定したり等改良の余地やプロジェクトによってレジストリを変更する場合もありそうです。
ここまで来るとinfraとbackendのGitHubリポジトリも分けて管理した方が良さそうです。

こちらもDockerの変更とLaravelのコード変更と分けてるので、開発しやすくなると思います。
また、本番環境も同じDockerイメージを利用できるので、エコな気がします。

本格的にやる場合はこのパターン3の方法がベストかなと思います。
しかし、プロジェクトによって様々設定が必要になると思うのでパターン3は参考程度にご利用ください。

## まとめ

ComposerのCache周りで少しハマりました。
キャッシュファイルが利用されるときは`composer dump-autoload`が実行されないので、別途ステップを作ってあげる必要がありました。

あとCI周り詳しくないので、もっと良い書き方とかあれば教えてください><。