GitHub ActionsでRailsのCIを実行して結果をslackに流す

もうすぐ正式リリースされるgithub actionsを使ってRailsアプリのCIがそれっぽく組めたので紹介します。
できたyamlは最後に載せています。

ワークフロー構文のドキュメントはこちら。

https://docs.github.com/ja/actions/reference/workflow-syntax-for-github-actions

2021/04 追記
actions/setup-rubyがアーカイブさたため、ruby/setup-rubyを使うように記事を修正しました。
また、その他バージョンアップなど細かな修正を行っています。

# 実行契機
PUSHの場合に実行します。
ただし、CIを回す必要がないREADMEやDockerfileなどのみ更新した時は動かしたくないので`paths-ignore`で除外しています。

```yaml
on:
  pull_request:
    paths-ignore:
      - README.md
      - docs/**
      - Dockerfile
      - docker-compose.yml
```

# チェック内容
RSpecを使って自動テスト、Rubocopを使ってコードの静的解析、Brakemanを使って脆弱性チェックをしています。
一昔前であればrails_best_practiceも入れていたところですが、1年くらい開発が止まっているので外しました。
bundler-auditも昔は入れていましたがgithubがSecurity Alertsを出してくれるようになったので外しました。

３つのチェックをジョブを分けることで並列で実行しています。

```yaml
jobs:
  rspec:
    ...
  rubocop:
    ...
  brakeman:
    ...
```

## RSpec
### 実行マシンの設定
まず最初に実行マシンの設定をします。
実行環境には環境変数を3つ設定しています。
RAILS_ENVは言わずもがな。
DB_HOST、DB_PORTは環境変数で設定するとその値を使ってDB接続するように`config/database.yml`に記載しています。

2021/04時点でubuntu-latestは20.04になりました。20.04だとMySQL5.7系が正しく動作しないため18.04を指定しています。

https://github.com/actions/virtual-environments/issues/1816

```yaml
  rspec:
    runs-on: ubuntu-18.04
    env:
      RAILS_ENV: test
      DB_HOST: 127.0.0.1
      DB_PORT: 33060
```

### mysqlの設定
DB接続が必要なのでmysqlを立ち上げています。
servicesで立ち上げたコンテナにはホストサーバーからはlocalhostでアクセスできるようです。
ちなみに前述している`DB_HOST: 127.0.0.1`は`DB_HOST: localhost`と書いても良さそうですが、mysqlの仕様でlocalhostだとソケット通信しようとしてしまうので`127.0.0.1`と指定しています。
portはデフォルトで良かったのですが`3306`は使われているようでエラーになったので`33060`にしました。
テスト環境なのでroot/（パスワードなし)
BIND-ADDRESSを設定しないとホストサーバーから接続できないので注意です。
optionsではヘルスチェックを指定しています。

```yaml
    services:
      db:
        image: mysql:5.7.27
        ports:
          - 33060:3306
        env:
          MYSQL_ALLOW_EMPTY_PASSWORD: yes
          BIND-ADDRESS: 0.0.0.0
        options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
```

### setup ruby / apt-get
コードのチェックアウト、rubyやライブラリのインストールをします。
以前はactions/setup-rubyが使われていましたが、ruby/setup-rubyが正式版となったのでこちらを使いましょう。

```yaml
    steps:
    - uses: actions/checkout@v2.3.4
    - name: Set up Ruby 2.7
      uses: ruby/setup-ruby@v1.68.0
      with:
        ruby-version: 2.7.2
        bundler-cache: true
```

### cache
先日github actionsにキャッシュが実装されたのでキャッシュを設定します。
これでyarn installがかなり高速化されます。
細かい設定は公式サイトをご覧ください。

https://github.com/marketplace/actions/cache

bundlerのキャッシュはruby/setup-rubyが行っているためこちらの指定は不要です。

```yaml
    - name: Cache node modules
      uses: actions/cache@v2.1.4
      with:
        path: node_modules
        key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}
        restore-keys: |
          ${{ runner.os }}-node-
    - name: bundle install
      run: |
        gem install bundler
        bundle install --jobs 4 --retry 3 --path vendor/bundle
    - name: yarn install
      run:  yarn install --check-files
```

下記のようにキャッシュの有無でかなり実行時間に差が出ます。
・キャッシュなし
<img width="686" alt="スクリーンショット 2019-11-05 16.18.34.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83424/fe874a68-e8c9-0679-15c4-2ddf8ebfb0ce.png">
・キャッシュあり
<img width="685" alt="スクリーンショット 2019-11-05 16.19.49.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83424/aea9ebfa-b4cc-807e-e511-35913bdc17de.png">

### migration / rspec
テストDBを生成してRSpecを実行しています。

```yaml
    - name: migration
      run: |
        bundle exec rails db:create
        bundle exec rails db:test:prepare
    - name: run rspec
      run: bundle exec rspec
```

## Rubocop、Brakeman
Rubocop、Brakemanは静的解析するだけなので、該当するgemだけインストールして実行。

```yaml
  rubocop:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2.3.4
    - name: Set up Ruby 2.7
      uses: ruby/setup-ruby@v1.68.0
      with:
        ruby-version: 2.7.2
        bundler-cache: true
    - name: run rubocop
      run: |
        gem install rubocop
        rubocop

  brakeman:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2.3.4
    - name: Set up Ruby 2.7
      uses: ruby/setup-ruby@v1.68.0
      with:
        ruby-version: 2.7.2
        bundler-cache: true
    - name: run brakeman
      run: |
        gem install brakeman
        brakeman -6 -A -w 1
```

# 結果通知
CIの実行結果を通知して欲しいのでSlack通知を設定しています。
良い記述方法がわからなかったので冗長ですが、成功・失敗の通知ジョブを作っています。
全てのチェックが終わってから通知したいので`needs`に先行ジョブを設定しています。
先行ジョブが1つでも失敗したら後続ジョブは動かないので`success_notify`は全ジョブが成功した時のみ動作します。
`failure_notify`は`if: failure()`をつけているので先行ジョブが1つでも失敗した場合に動作します。

slack通知はマーケットプレイスにあった下記を使いました。
https://github.com/marketplace/actions/slack-notify
細かい設定は公式サイトをご覧ください。

```yaml
  success_notify:
    needs: [rspec, rubocop, brakeman]
    runs-on: ubuntu-latest
    steps:
    - name: slack notify
      uses: rtCamp/action-slack-notify@v2.1.3
      env:
        SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
        SLACK_MESSAGE: 'Post Content :rocket:'
        SLACK_CHANNEL: ci

  failure_notify:
    if: failure()
    needs: [rspec, rubocop, brakeman]
    runs-on: ubuntu-latest
    steps:
    - name: slack notify
      uses: rtCamp/action-slack-notify@v2.1.3
      env:
        SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
        SLACK_MESSAGE: 'Post Content :rocket:'
        SLACK_CHANNEL: ci
        SLACK_COLOR: '#FF0000'
```

# バッジ
せっかくCIを回しているのであればREADMEにバッジをつけたいですよね。
<img width="123" alt="スクリーンショット 2019-11-05 16.47.14.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83424/72dce8f5-61aa-7dab-4ecc-d0f2c3d73139.png">

バッジのURLはActionsタブで対象のActionを選び、右上の三点リンクの"Create status badge"で生成できます。
コピーしてREADMEにでも貼っておきましょう。

![スクリーンショット 2021-04-07 14.01.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83424/8c92c433-5adb-90b5-2f35-b30118c855ac.png)

# 検証に使ったyaml

```yaml:.github/workflows/ruby.yml
name: Ruby

on:
  pull_request:
    branches:
      - 'feature/*'
      - main
    paths-ignore:
      - README.md
      - docs/**
      - Dockerfile
      - docker-compose.yml

jobs:
  rspec:
    runs-on: ubuntu-18.04
    env:
      RAILS_ENV: test
      DB_HOST: 127.0.0.1
      DB_PORT: 33060
    services:
      db:
        image: mysql:5.7.27
        ports:
          - 33060:3306
        env:
          MYSQL_ALLOW_EMPTY_PASSWORD: yes
          BIND-ADDRESS: 0.0.0.0
        options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
    steps:
    - uses: actions/checkout@v2.3.4
    - name: Set up Ruby 2.7
      uses: ruby/setup-ruby@v1.68.0
      with:
        ruby-version: 2.7.2
        bundler-cache: true
    - name: Cache node modules
      uses: actions/cache@v2.1.4
      with:
        path: node_modules
        key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}
        restore-keys: |
          ${{ runner.os }}-node-
    - name: bundle install
      run: |
        gem install bundler
        bundle install --jobs 4 --retry 3 --path vendor/bundle
    - name: yarn install
      run:  yarn install --check-files
    - name: migration
      run: |
        bundle exec rails db:create
        bundle exec rails db:test:prepare
    - name: run rspec
      run: bundle exec rspec

  rubocop:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2.3.4
    - name: Set up Ruby 2.7
      uses: ruby/setup-ruby@v1.68.0
      with:
        ruby-version: 2.7.2
        bundler-cache: true
    - name: run rubocop
      run: |
        gem install rubocop
        rubocop

  brakeman:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2.3.4
    - name: Set up Ruby 2.7
      uses: ruby/setup-ruby@v1.68.0
      with:
        ruby-version: 2.7.2
        bundler-cache: true
    - name: run brakeman
      run: |
        gem install brakeman
        brakeman -6 -A -w 1

  success_notify:
    needs: [rspec, rubocop, brakeman]
    runs-on: ubuntu-latest
    steps:
    - name: slack notify
      uses: rtCamp/action-slack-notify@v2.1.3
      env:
        SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
        SLACK_MESSAGE: 'Post Content :rocket:'
        SLACK_CHANNEL: ci

  failure_notify:
    if: failure()
    needs: [rspec, rubocop, brakeman]
    runs-on: ubuntu-latest
    steps:
    - name: slack notify
      uses: rtCamp/action-slack-notify@v2.1.3
      env:
        SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
        SLACK_MESSAGE: 'Post Content :rocket:'
        SLACK_CHANNEL: ci
        SLACK_COLOR: '#FF0000'
```