GitHub Actions で PHP の CI/CD をする

[EC-CUBE](https://github.com/EC-CUBE/ec-cube) の CI/CD を [GitHub Actions](https://help.github.com/ja/articles/about-github-actions) で実装してみたので、メモ程度に知見をまとめてみます。

該当の Pull Request はこちら
- 4系 https://github.com/EC-CUBE/ec-cube/pull/4337
- 2系 https://github.com/EC-CUBE/eccube-2_13/pull/312 

*2019年12月現在、 [EC-CUBE organization アカウントが利用している契約プランの関係](https://github.com/EC-CUBE/ec-cube/issues/4392#issuecomment-558489494) で、 EC-CUBEオフィシャルリポジトリで GitHub Actions は利用できなくなっています。 ご利用の際は、 fork した個人アカウントでお願いします*

## 実現したいこと

- 以下の環境をマトリクスで実行したい
  - Linux, Windows
  - PHP5.4〜7.3
  - PostgreSQL/MySQL/SQLite3
- PHPUnit によるユニットテスト
- Codeception による E2Eテスト

EC-CUBE は、Travis CI 及び AppVeyor で CI を実行しており、これを GitHub Actions へ移植することにします

## 直面した課題

### GitHub Actions にインストールされている PHP バージョン

- ubuntu-18.04 は PHP7.1〜7.3
- ubuntu-16.04 は PHP5.6〜7.3
- windows-2019, windows-2016 は PHP7.3

上記のような状況ですので、 PHP5.4, 5.5 の環境は自力で構築する必要があります。
GitHub Actions は、独自の Actions を作成できますので、 PHP5.4〜7.3 を Linux, Windows でマトリクス実行する Actions を作成しました。

**[Setup PHP environment](https://github.com/marketplace/actions/setup-php-environment)**

以下のような感じで実行できます

```yaml

jobs:
  build:
    runs-on: ${{ matrix.operating-system }}
    strategy:
      matrix:
        operating-system: [ ubuntu-18.04, windows-2019 ]
        php: [ '5.4', '5.5', '5.6', '7.1', '7.2', '7.3', '7.3.3' ]
    name: PHP ${{ matrix.php }} sample
    steps:
      - uses: actions/checkout@master
      - name: Setup PHP
        uses: nanasess/setup-php@master
        with:
          php-version: ${{ matrix.php }}
      - run: php my_script.php
```
- Linux で apt の利用可能なバージョンは apt を使用
  - apt が利用できない場合は phpenv でビルド
- Windows 環境は chocolatey を利用
- ubuntu-16.04 は標準パッケージと小々異なる構成(OpenSSL1.1 がインストールされている)のため、 OpenSSL1.0 と libpq.so をソースから構築している

独自の Actions を作りたい場合は、 JavaScript または Docker コンテナを使用する必要があります。 [actions/typescript-action](https://github.com/actions/typescript-action/) テンプレートや [actions/javascript-action](https://github.com/actions/javascript-action/), [actions/hello-world-docker-action](https://github.com/actions/hello-world-docker-action) が利用できます。

### Windows 環境は Composer がインストールされていない

こちらも独自の Actions を作成しました

**[Composer installer of Github Actions](https://github.com/marketplace/actions/composer-installer-action)**

### ChromeDriver の実行

Codeception を実行するために ChromeDriver を設定する必要があります。
以下のような step を作成しました

```yaml
    - name: Setup chromedriver
      run: |
        sudo apt-fast install -y xvfb debconf-utils screen google-chrome-stable
        wget -c -nc --retry-connrefused --tries=0 http://chromedriver.storage.googleapis.com/2.43/chromedriver_linux64.zip
        unzip -o -q chromedriver_linux64.zip
        export DISPLAY=:99
        ./chromedriver --url-base=/wd/hub &
        echo ">>> Started chrome-driver"
        sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 &

```

ここで注意する必要があるのが、環境変数 DISPLAY の設定です。
[`jobs.<job_id>.steps.env`](https://help.github.com/ja/articles/workflow-syntax-for-github-actions#jobsjob_idstepsenv) で設定しても、 ChromeDriver が動きません。
`export` で設定する必要があります。

ちなみに、ファイルダウンロードのテストをする必要があるため、ヘッドレスモードは使用しません。

2019年10月3日追記 Actions を作成しました
**[nanasess/setup-chromedriver](https://github.com/nanasess/setup-chromedriver)**

こんな感じで記述できます

```yaml
    - name: setup-chromedriver
      uses: nanasess/setup-chromedriver@master
      with:
        chromedriver-version: '77.0.3865.40'

    - name: Run chromedriver
      run: |
        export DISPLAY=:99
        chromedriver --url-base=/wd/hub &
        echo ">>> Started chrome-driver"
        sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 &
        echo ">>> Started xvfb"
```

### Windows 環境での MySQL のテスト

Linux 環境では MySQL Server が動いているのですが、 Windows 環境ではクライアントしかインストールされてないため、自前でインストールします

```yaml
    - name: Setup to database
      run: |
        choco install -y mysql --version 5.7.18
        mysql --user=root -e "CREATE DATABASE `myapp_test`;"
        mysql --user=root -e "ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password';FLUSH PRIVILEGES;"
        mysql --user=root --password=password -h 127.0.0.1 -e "SELECT version();"
```

### PostgreSQL のテスト

Linux では [`jobs.<job_id>.services`](https://help.github.com/ja/articles/workflow-syntax-for-github-actions#jobsjob_idservices) で PostgreSQL を実行できます

```yaml
    services:
      postgres:
        image: postgres:11
        env:
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: password
          POSTGRES_DB: postgres
        ports:
          - 5432:5432
        # needed because the postgres container does not provide a healthcheck
        options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
```

参考 https://github.com/actions/example-services/blob/master/.github/workflows/postgres-service.yml

### MailCatcher を使用したテスト

Linux では [`jobs.<job_id>.services`](https://help.github.com/ja/articles/workflow-syntax-for-github-actions#jobsjob_idservices) で Docker コンテナを実行します

```yaml
    services:
      mailcatcher:
        image: schickling/mailcatcher
        ports:
          - 1080:1080
          - 1025:1025
```

Windows は gem でインストールします。
`shell: bash` を指定するのがポイント。デフォルトの cmd ではバックグラウンドで実行できません。。。

```yaml
    - name: Setup MailCatcher
      run: gem install -N mailcatcher -v 0.6.5
      shell: bash
    - name: Run to MailCatcher
      run: mailcatcher &
      shell: bash
```

### マイクロサービスの実行

EC-CUBE の E2E テストでは、 [EC-CUBEオーナーズストア](https://www.ec-cube.net/owners/) をエミュレートした Docker コンテナを実行し、プラグインインストールなどのテストをしています。
これを [`jobs.<job_id>.services`](https://help.github.com/ja/articles/workflow-syntax-for-github-actions#jobsjob_idservices) で動かしたかったのですが、、 Volume マウントするディレクトリが services を初期化する段階では生成できないため、 docker run コマンドで実行するしかないようです

```yaml
    ## $ {PWD}/repos does not exist so service cannot be started
    - name: Run package-api
      run: docker run -d --rm -v ${PWD}/repos:/repos -e MOCK_REPO_DIR=/repos -p 8080:8080 eccube/mock-package-api
```

本当は以下のように書きたい

```yaml
    services:
      package-api:
        image: eccube/mock-package-api
        env:
          MOCK_REPO_DIR: '/repos'
        ports:
          - 8080:8080
        volumes:
          - $GITHUB_WORKSPACE/repos:/repos
```

## イベントのフィルタリング

- Pull Request を master にマージした場合にワークフローが2つ走らないようにしたい
- `*.md` ファイルのみの修正はワークフローを実行しないようにしたい
- リリースを発行した時にワークフローを実行する
- tag を打った時にワークフローを実行する
- 基本的に直接 push しない

以下のようにすることで、うまく動作しています

```yaml
on:
  push:
    branches:
      - master
    tags:
      - '*'
    paths:
      - '**'
      - '!*.md'
  pull_request:
    paths:
      - '**'
      - '!*.md'
  release:
    types: [ published ]

```

## デプロイする

GitHub Actions には AWS CLI や Azure CLI コマンドもインストールされているため、各種クラウドへのデプロイも柔軟にできます。

ここでは、 GitHub にて release の発行をすると、自動的に ZIP 及び tar.gz のパッケージを生成し、 Assets にアップロードするようにしました。

<img width="1003" alt="スクリーンショット 2019-09-28 0.59.27.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40662/e349dded-beaf-137d-406b-640d1afec77f.png">

コードは長いので [こちら](https://github.com/EC-CUBE/ec-cube/blob/5e2226050020f3f881912e5f31583378671d9cc2/.github/workflows/action.yml#L637-L798) を参照してください。

ポイントは、以下のようにすることで、タグ名をパッケージのファイル名にできます

```yaml
    - name: Packaging
      working-directory: ../
      env:
        TAG_NAME: ${{ github.event.release.tag_name }}
        REPOSITORY_NAME: ${{ github.event.repository.name }}
      run: |
        tar czfp eccube-$TAG_NAME.tar.gz $REPOSITORY_NAME
        zip -ry eccube-$TAG_NAME.zip $REPOSITORY_NAME 1> /dev/null
```

`github.event` オブジェクトには、多くの情報が格納されています。
以下のようにして参照できます

```yaml
    - name: Dump GitHub context
      env:
        GITHUB_CONTEXT: ${{ toJson(github) }}
      run: echo "$GITHUB_CONTEXT"
    - name: Dump job context
      env:
        JOB_CONTEXT: ${{ toJson(job) }}
      run: echo "$JOB_CONTEXT"
    - name: Dump steps context
      env:
        STEPS_CONTEXT: ${{ toJson(steps) }}
      run: echo "$STEPS_CONTEXT"
    - name: Dump runner context
      env:
        RUNNER_CONTEXT: ${{ toJson(runner) }}
      run: echo "$RUNNER_CONTEXT"
    - name: Dump strategy context
      env:
        STRATEGY_CONTEXT: ${{ toJson(strategy) }}
      run: echo "$STRATEGY_CONTEXT"
    - name: Dump matrix context
      env:
        MATRIX_CONTEXT: ${{ toJson(matrix) }}
      run: echo "$MATRIX_CONTEXT"
```

## カバレッジを計測する

`GITHUB_TOKEN` でカバレッジも連携できたら便利ですよね。

[Coveralls GitHub Action](https://github.com/marketplace/actions/coveralls-github-action) は LCOV 型式のみのサポートの模様。
PHPUnit から LCOV 形式のレポートは出力できないため、 PHP のプロジェクトを `secrets.GITHUB_TOKEN` で連携するのは今のところ難しそうです。
(Coveralls.io の token を登録すれば、 php-coveralls で連携できます)

[codecov](https://github.com/codecov/codecov-action) も `GITHUB_TOKEN` での連携はサポートされてない模様

## まとめ

まだ始まったばかりのサービスですし、 PHP で CI/CD をする環境が充実しているとは言い難い感じですが、いろいろ頑張れば柔軟に対応できそうです。

*またナレッジがたまったら追記します*

## 参考
- [GitHub Actions の仮想環境でのソフトウェア](https://help.github.com/ja/articles/software-in-virtual-environments-for-github-actions)