「GitLab CI」「GitHub Actions」CIを行う方法を比較

# はじめに
先日、GitHubの料金体系が変更されましたね！
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Today we’re announcing free private repositories with unlimited collaborators for teams with GitHub Free, and reducing the price of our paid Team plan to $4 per user/month. All of the core GitHub features are now free for everyone. <br><br>Learn more: <a href="https://t.co/fQ3r2ABtTR">https://t.co/fQ3r2ABtTR</a> <a href="https://t.co/HHOyG5ypgU">pic.twitter.com/HHOyG5ypgU</a></p>&mdash; GitHub (@github) <a href="https://twitter.com/github/status/1250092060339773441?ref_src=twsrc%5Etfw">April 14, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

個人的にずっとGitLabをメインに利用していましたが、今後はGitHubメインで使おうと考えています。
そこで、GitLab CIで行っていたワークフローをGitHub Actionsへ移行しました。
GitLab CIからGitHub Actionsへ、GitHub ActionsからGitLab CIへ移行を考えている方の参考になれば幸いです。

# サンプル
今回サンプルで作成した、GitLab CIとGitHub Actionsの設定ファイルはこちらです。
https://gist.github.com/urbsny/ddb634e5f677e1d4d312f4a86916adc8

# CIで行うワークフローについて
Djangoで作成されたwebアプリのテストしデプロイします。ざっくり下記の様な処理を行います。

1. `git push`をトリガーとしてCIを実行
1. テストを実行し、カバレッジを出力する。
1. masterへのpushの場合、herokuへデプロイする。

それでは、GitLabとGitHubそれぞれの設定を見ていきます。

## GitLabの設定
これらのフローをGitLabではステージ(stage)と表現されています。
デフォルトでは、`build` -> `test` -> `deploy`というステージが用意されています。`build`が成功したら`test`、`test`が成功したら`deploy`が実行されます。
今回のサンプルでは、`test`と`deploy`のステージのみを使用しました。下記、ステージに関する設定をピックアップしています。

```yml
test:
  script:
    - echo TODO
deploy-staging:
  stage: deploy
  only:
    - master
  script:
    - echo TODO
deploy-production:
  stage: deploy
  only:
    - tags
    - /^v[0-9]+\.[0-9]+\.[0-9]+$/
  script:
    - echo TODO
```

`test：`は毎回実行されます。 `deploy-staging:`はmasterへのpushの場合のみ実行されます。 `deploy-production:`はタグ打ちした場合のみ実行されます。

`test：`では`stage:`を指定していません。ステージ名とキー同じ場合はステージの指定は不要なようです。

デフォルトで用意されているステージを使用せず、独自に定義することも可能です。詳しくは、下記を参照しましょう。
https://docs.gitlab.com/ee/ci/yaml/#stages

## GitHubの設定
一方GitHubでは、それぞれのフローをジョブ(job)として表現しています。ただ、GitLabと違いデフォルトのステージのようなものはありません。
GitHubでは、 `needs:`でステップの依存関係を指定する必要があります。

```yml
name: CI
on:
  push:
    branches:
      - '*'
    tags:
      - 'v[0-9]+.[0-9]+.[0-9]+'
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - run: echo TODO
  deploy-staging:
    needs: test
    if: github.ref == 'refs/heads/master'
    runs-on: ubuntu-latest
    steps:
      - run: echo TODO
  deploy-production:
    needs: test
    if: startsWith(github.ref, 'refs/tags/v')
    runs-on: ubuntu-latest
    steps:
      - run: echo TODO
```

また、トリガーとなるアクションも `on:`で定義する必要があります。サンプルではブランチ全般とv1.0.0等のバージョンタグの場合に限定しています。詳しくは、下記を参照しましょう。
https://help.github.com/ja/actions/reference/workflow-syntax-for-github-actions

特定の条件で実行したジョブの条件は、 `if:`で定義します。`if:`で使用可能な関数や変数はこちらを参照しましょう。
https://help.github.com/ja/actions/reference/context-and-expression-syntax-for-github-actions

# テストフロー (Djangoでのテスト)
Pythonの実行環境でDjangoのテストを実行します。その際にPostgreSQLとRedisを使用する為、その設定も必要になります。また、テストのカバレッジを出力し、ダウンロードできるようにしています。

## GitLabの設定
GitLabで特定のPythonのバージョンでテストを行う為には、dockerコンテナ上で実行するのが手っ取り早いと思います。 `image:`でdockerイメージを指定します。

```yml
test:
  image: python:3.8.2
  services:
    - name: postgres:12.2
      alias: database
    - name: redis:5.0.8
      alias: cache
  variables:
    POSTGRES_PASSWORD: xxxxxxxx
    DATABASE_URL: postgres://postgres:xxxxxxxx@database:5432/postgres
    REDIS_URL: redis://cache:6379
    SAMPLE_VARIABLE: 1
  script:
    - python -m pip install --upgrade pip
    - pip install pipenv
    - pipenv install -d
    - pipenv run collectstatic
    - pipenv run testcoverage
    - pipenv run coveragehtml
  artifacts:
    paths:
      - htmlcov/
```

PostgreSQLとRedisの設定を `services:`で定義します。この中の `alias:`で設定された値とアプリ側のPostgreSQLやRedisのホスト名を一致させる必要があります 。aliasを使用しないことも可能ですが、その場合は下記のようにサービスのイメージ名を指定します。

```yml
services:
  - postgres:12.2
  - redis:5.0.8
variables:
    DATABASE_URL: postgres://postgres:xxxxxxxx@postgres:5432/postgres
    REDIS_URL: redis://redis:6379
```

必要な環境変数は、 `variables:`で定義します。今回のサンプルにある `DATABASE_URL:` `REDIS_URL:` `SAMPLE_VARIABLE:`はサンプルのwebアプリが利用する特有の定義です。ただし、`POSTGRES_PASSWORD:`はPostgreSQLのサービスが使用する環境変数になります。適当な値を設定し、アプリ側のPostgreSQLのパスワードと一致させましょう。
テストに必要な処理は、`script:`に記載します。サンプルではPipenvを使用しPipfileに独自に定義したスクリプトを実行しています。requirements.txtを使用する場合は下記の様なスクリプトになるかと思います。

```yml
script:
    - python -m pip install --upgrade pip
    - pip install -r requirements.txt
    - ./manage.py collectstatic --noinput
    - pytest -n auto --cov
    - coverage html
```

最後に、`coverage html`コマンドで出力されたカバレッジ結果をダウンロードできるようにします。`./htmlcov`配下に出力される為、下記のような定義になります。

```yml
artifacts:
    paths:
      - htmlcov/
```

## GitHubの設定
GitHubでは特定のPythonのバージョンでテストを行う上で、dockerコンテナを使用しなくても簡単に環境を構築できます。`uses: actions/setup-python@v1`がその為の処理になります。この場合、PostgreSQLやRedisのホストを`localhost`にする必要があります。

```yml
test:
    runs-on: ubuntu-latest
    services:
      database:
        image: postgres:12.2
        env:
          POSTGRES_PASSWORD: xxxxxxxx
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
      cache:
        image: redis:5.0.8
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    env:
      DATABASE_URL: postgres://postgres:xxxxxxxx@localhost:5432/postgres
      REDIS_URL: redis://localhost:6379
      SAMPLE_VARIABLE: 1
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-python@v1
        with:
          python-version: 3.8.2
      - run: python -m pip install --upgrade pip
      - run: pip install pipenv
      - run: pipenv install -d
      - run: pipenv run collectstatic
      - run: pipenv run testcoverage
      - run: pipenv run coveragehtml
      - uses: actions/upload-artifact@v1
        with:
          name: coverage
          path: htmlcov/
```

dockerコンテナで実行する場合は、`container:`を指定し、ホストの指定をlocalhostから`services:`で指定した名前にします。

```yml
runs-on: ubuntu-latest
    container: python:3.8.2
    services:
      database:
        image: postgres:12.2
        env:
          POSTGRES_PASSWORD: xxxxxxxx
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432
      cache:
        image: redis:5.0.8
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 6379:6379
    env:
      DATABASE_URL: postgres://postgres:xxxxxxxx@database:5432/postgres
      REDIS_URL: redis://cache:6379
    steps:
      - uses: actions/checkout@v2
      - run: python -m pip install --upgrade pip
```

# デプロイフロー (Herokuへのデプロイ)
HerokuへのデプロイにはDplというGemを使用しています。
Herokuへのデプロイに使用する機微情報はソースにコミットせずに、それぞれの管理画面から設定した環境変数で利用します。サンプルでは、`HEROKU_APP` `HEROKU_API_KEY` という値を設定しています。

## GitLabの設定
```yml
deploy-staging:
  stage: deploy
  script:
    - apt-get update -qy
    - apt-get install -y ruby-dev
    - gem install dpl
    - dpl --provider=heroku --app=$HEROKU_APP --api-key=$HEROKU_API_KEY
```
環境変数の設定は、再度メニューのSettings > CI/CDを開きVariablesのメニューから追加できます。
https://docs.gitlab.com/ee/ci/variables/README.html#via-the-ui

## GitHubの設定

```yml
deploy-staging:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - run: sudo apt-get update -qy
      - run: sudo apt-get install -y ruby-dev
      - run: sudo gem install dpl
      - run: sudo dpl --provider=heroku --app=$HEROKU_APP --api-key=$HEROKU_API_KEY
        env:
          HEROKU_APP: ${{ secrets.HEROKU_APP }}
          HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }}
```

環境変数の設定は、下記のサイトを参考にします。
https://help.github.com/ja/actions/configuring-and-managing-workflows/creating-and-storing-encrypted-secrets#creating-encrypted-secrets

# 最後に
個人的にはGitLab CIの設定の方が好みでしたが、GitHub Actionsは公開されているActionsを使用すれば簡単に色々できそうです。
https://github.com/marketplace?type=actions

GitLabのRepository mirroringをしようすれば、それぞれの環境で別々のCIを回すなんてこともできそうですね。
https://docs.gitlab.com/ee/user/project/repository/repository_mirroring.html