こんにちは、AutoScale の小西です。
うちの会社の自社サービス「SocialDog」では、これまで CI ツールとして、CircleCI を使ってきました。
今回 GitHub Actions に乗り換えたので、CircleCI と GitHub Actions の違いを見つつ、設定ファイルを晒してみたいと思います。
CI 組むとき、説明読むより、サンプルがある早いですよね。 というわけでうちで使っているものをそのまま晒してみます。
React+TypeScript とか書いたものの、ほぼ yarn のコマンドに集約されちゃっててあんまり関係ないなw
- 乗り換え理由
- 乗り換え手順
- ディレクトリ構成
- GitHub Actions の設定ファイル
- 動作確認
- GitHub Actions の設定ファイルの書き方
- GitHub Actions のイケてるところ
- GitHub Actions の残念なところ
- まとめ
乗り換え理由
これは単純で、うちの場合試算したら、CircleCI よりも GitHub Actions のほうが料金が安かったからです。
すでに GitHub Team を使っている場合、料金の面で有利な場合は多いんじゃないかな、ぜひ試算をオススメ。
また仮に料金がほぼ同じだったとしても、料金、機能がそんなに変わらなければ、使うツールは少ないほうが良いと考えました。
乗り換え手順
- 一通りドキュメントを読む
なんと日本語ドキュメントがあります。
GitHub Actionsを使ってみる - GitHub ヘルプ - Workflow 定義の yaml を書く
.github/workflows/xxx.yaml
に保存して push し、Pull Request を作成((onにpushがあればPullRequestの作成は必須ではありませんが、github.head_ref
などは中身が違うので注意。))- GitHub Actions の画面で動作確認する
ディレクトリ構成
SocialDog のリポジトリは、①webバックエンド・フロントエンド、②ネイティブアプリの2つのディレクトリからなるmonorepoとなっています*1。 それぞれのフォルダに package.json があり、JS は yarn, PHP は composer で依存関係を管理しています。
. ├── .github │ └── workflows │ └── main.yml (←今回書く設定ファイル) ├── native │ ├── [ReactNative 関連のファイル] │ ├── package.json │ └── yarn.lock └── web ├── application (CodeIgniterのファイル) ├── build (phpunitなどの青果物が入るフォルダ) ├── composer.json ├── composer.lock ├── cypress ├── cypress.json ├── package.json ├── public └── yarn.lock
GitHub Actions の設定ファイル
GitHub Actions も CircleCI と同様、yaml ファイルに設定を書きます。
ファイルの場所は、 .github/workflows/xxx.yaml
となります。
書き方も .circle/config.yml
とよく似ているので、移行はしやすそうです。
.github/workflows/xxx.yaml
name: GitHub Actions CI on: push: branches: - master pull_request: jobs: php: name: PHP (web) runs-on: ubuntu-latest services: mysql: image: mysql:5.7 env: MYSQL_ALLOW_EMPTY_PASSWORD: false MYSQL_ROOT_PASSWORD: password MYSQL_DATABASE: cheetah_test ports: - 13306:3306 options: --health-cmd="mysqladmin ping" --health-interval=30s --health-timeout=30s --health-retries=10 env: DATABASE_HOST: mysql:13306 steps: - name: Prepare / Checkout uses: actions/checkout@v2 - name: Prepare / Setup PHP uses: shivammathur/setup-php@v2 with: php-version: 7.4 extensions: mbstring, intl, pdo_mysql, mysqli coverage: xdebug ini-values: memory_limit=512M, short_open_tag=On - name: Prepare / Get composer cache directory id: composer-cache run: cd web; echo "::set-output name=dir::$(composer config cache-files-dir)" - name: Prepare / Cache composer dependencies uses: actions/cache@v1 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} restore-keys: ${{ runner.os }}-composer- - name: Prepare / Composer Install run: | cd web; composer install --no-progress --no-suggest --prefer-dist --optimize-autoloader - name: Prepare / Initialize Database run: | echo "<?php define('ENVIRONMENT', 'testing');" > web/environment.php; cd web/public; php index.php cli/dev init_database_for_testing - name: PHP / PHPUnit run: | cd web/application/tests ; ../../vendor/bin/phpunit -c phpunit.CircleCI.xml - name: PHP / phpmd if: always() run: cd web; composer phpmd - name: Translation / Check if: always() run: web/dev_tools/check_translation.php - name: Save / CodeCov run: bash <(curl -s https://codecov.io/bash) -t xxxxxxxxxxxxxxxxxxxxx js: name: JS (web/native) runs-on: ubuntu-latest env: NODE_ENV: production GOOGLE_SERVER_ACCOUNT_JSON_BASE64: ${{ secrets.GOOGLE_SERVER_ACCOUNT_JSON_BASE64 }} GOOGLE_APPLICATION_CREDENTIALS: /home/runner/gcloud-service-key.json SENTRY_ORG: xxxxxxxx SENTRY_PROJECT: xxxxxxxx SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SHA: ${{ github.sha }} steps: - name: Prepare / Checkout uses: actions/checkout@v1 - name: Prepare / Install for cypress run: sudo apt-get install -y libgtk2.0-0 libgtk-3-0 libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb fonts-noto-cjk - name: Prepare / Cache .cache directory uses: actions/cache@v1 id: yarn-cache with: path: ~/.cache key: cache5-${{ hashFiles('web/yarn.lock') }}${{ hashFiles('native/yarn.lock') }} restore-keys: | cache5- - name: Prepare / Use Node.js v10.15.3 uses: actions/setup-node@v1 with: node-version: v10.15.3 - name: Prepare / Yarn Install web working-directory: ./web run: yarn install - name: Prepare / Yarn Install native working-directory: ./native run: yarn install --ignore-scripts && yarn patch-package - name: Web / Build JavaScript / SCSS run: cd web; yarn run build - name: Web / Prettier Check if: always() run: cd web; yarn run prettier-check - name: Web / ESlint TestCode(web) if: always() run: cd web; yarn run eslint-test-js - name: Web / Jest (JavaScript Test) if: always() run: cd web; NODE_ENV=test yarn run jest - name: Web / Stylelint if: always() run: cd web; yarn run stylelint-check - name: Web / Stylelint for Styled Components StyleSheet if: always() run: cd web; yarn run stylelint-styled - name: Web / Prettier Check if: always() run: cd web; yarn run prettier-check - name: Web / master_data Check if: always() run: | mv native/js/master_data.ts native/js/master_data_this_commit.ts cd web; yarn run build:master_data cd ..; diff -q native/js/master_data.ts native/js/master_data_this_commit.ts - name: Web / Cypress Server run: cd web; yarn run cypress_server_start & - name: Web / Cypress Run run: cd web; yarn run cypress run - name: Native / Prettier Check if: always() run: cd native; yarn run prettier-check - name: Native / ESLint if: always() run: cd native; yarn run eslint-all-js - name: Native / TypeScript Check if: always() run: cd native; yarn tsc - name: Native / Stylelint if: always() run: cd native; yarn all-stylelint - name: Prepare / GCP SDK Install if: always() uses: GoogleCloudPlatform/github-actions/setup-gcloud@master with: version: '281.0.0' - name: Save / GCP Credential if: always() run: | echo $GOOGLE_SERVER_ACCOUNT_JSON_BASE64 | base64 --decode --ignore-garbage > ${HOME}/gcloud-service-key.json sudo gcloud auth activate-service-account --key-file ${HOME}/gcloud-service-key.json sudo gcloud config set project xxxxxxxx # @see https://github.com/reg-viz/reg-suit#workaround-for-detached-head - name: Save / reg-suit if: always() run: | cd web; git checkout ${{ github.ref }} || git checkout -b ${{ github.ref }} yarn reg-suit run - name: Save / CodeCov if: always() run: bash <(curl -s https://codecov.io/bash) -t xxxxxxxxxxxxx - name: Save / Upload Static files to GCS (only master) if: github.ref == 'refs/heads/master' run: | sudo gsutil cp -r web/public/build gs://xxxxxxxxx/$SHA - name: Save / Send release to Sentry (only master) if: github.ref == 'refs/heads/master' run: | curl -sL https://sentry.io/get-cli/ | bash sentry-cli releases new $SHA sentry-cli releases set-commits "$SHA" --auto sentry-cli releases finalize "$SHA"
動作確認
↑上記を保存してPullRequestを作成すると、実行されます。
Actions
のタブで動作をリアルタイムに確認できます。
Pull Request などの画面ではこんな感じで表示されます。大きくは変わりません(下のはCircleCI からのものです)。
GitHub Actions の設定ファイルの書き方
詳細は公式ドキュメントを参照。なんと日本語版があります*2。
GitHub Actionsのワークフロー構文 - GitHub ヘルプ
ここではハマりそうな箇所などを中心にメモしときます。
on
このワークフローを実行するタイミングを選べます。Pull Reqeust を出したときだけではなく、Push したタイミング、コメントをしたタイミングなど、GitHub 上でのアクションほぼ全部を起点としてワークフローを実行できます。
特定のブランチのみ、などの条件が CircleCI よりも書きやすいです。
jobs
ここにジョブを書きます。CircleCI で言うところの「Workflow」にあたるものです。 このジョブが実行される単位になります。ジョブが2つあれば同時実行ジョブ2消費する感じです。
services
DBやキャッシュなど、依存するサービスを起動しておくことができます。
docker-compose と設定が似ています。
ports
で設定したポートにバインドされます。上記の mysql
の設定だと、アプリのコードからは以下でアクセスできます。
ホスト名: mysql
(サービス名)
ポート: 13306
ユーザー名: root
パスワード: password
steps
それぞれのステップを記載します。
run
を使って、シェルのコマンドをそのまま書くこともできますし、uses
を使って、先人たちが作ってくれたものを使うこともできます。
チェックアウト(actions/checkout@v2)
GitHub 公式のソースコードのチェックアウトのタスク actions/checkout@v2
は、少し動作に癖があるので注意が必要です。
Pull Request の場合、トピックブランチのコードにマージ先のブランチをmergeした新しいブランチ(PR マージブランチ refs/pull/:prNumber/merge)が自動的に作られます。
PR#1234 でトピックブランチ fix1234
→ master
というPullRequest の場合*3、fix1234
に master
をマージした新しいブランチ refs/pull/1234/merge
が作られます。
他のCIから移す際に現在のブランチ名に依存したコードがあるとこれのせいでコケます(後述の reg-suit)。
詳細は以下のページ参照。
ワークフローをトリガーするイベント - GitHub ヘルプ
言語環境のインストール(actions/setup-node , shivammathur/setup-php@v2)
先人たちのおかげで非常に楽にインストールできます。
PHPのコードカバレッジツールに xdebug を使っていますが、pcovのほうが性能が良いらしいので移行したい*4
キャッシュ (actions/cache@v1)
指定したパスをキャッシュできます。ここはCircleCIの restore_cache
と非常によく似ています。
yarn のキャッシュは、node_modulesではなく、~/.cache
で行います。
秘密の文字列 (Secrets)
Setting → Secrets から登録しておいた文字列は、 ${{ secrets.GOOGLE_SERVER_ACCOUNT_JSON_BASE64 }}
のようにしてアクセスできます。
GCPやSentryなどの認証情報を入れときます。
CircleCI と違い、環境変数として渡されるわけではないので、上記の書き方で取得する必要があります。
Cypress
Cypress は runs-on: ubuntu-latest
で使うとうまく動かない場合があるらしいので、v3.8.3以降にするか、 ubuntu-16.04
を使うかしろとのことなので、素直に従います。
We are getting reports that Cypress has suddenly started crashing when running on ubuntu-latest OS. Seems, GH Actions have switched from 16.04 to 18.04 overnight, and are having a xvfb issue. Please work around this problem by using runs-on: ubuntu-16.04 image or upgrading to Cypress v3.8.3 where we explicitly set XVFB arguments. https://github.com/cypress-io/github-action#important
バックグラウンドタスク
GitHub Actions には、バックグラウンドで動かす、というフラグ(CircleCI で言うところの、 background: true
にあたるもの)がありません。
以下のように &
で動かせばOKです。
run: cd web; yarn run cypress_server_start &
ただこの方法だとログが見られないので、テキストファイルに保存しておいて Artifacts に出すなどの工夫が必要そうです。
Artifacts (actions/upload-artifact@v1)
テスト結果などをArtifacts として保存しておく仕組みがあります。 使い方はCircleCI と同じなのですが、ストレージ料金がかかります! しかもそれが結構高くて、$0.25 / GB します!
CircleCI ではここは課金されなかったので、導入を検討する場合は、ここも試算に入れるのを忘れないようにします。
ジョブ完了後に Artifacts としてダウンロードできるのですが、勝手にzipにまとめられた状態でダウンロードする形になります。 そのため CircleCIのように、HTMLなどを置いといてURL開くだけでカバレッジレポートを確認する、という使い方はできません。
SocialDog ではPHPUnit の結果のHTML形式のカバレッジレポートを吐き出していたのですが、その容量が1回あたり100MBほどありました。これだと1日30回=月600回だと$150とそこそこお金かかっちゃいます。
なのでこれは使わずに、AWSのS3やGCPのGCSを使うのが定石になりそうです*5。値段も$0.03/GBとかで圧倒的に安いし。
ちなみにストレージの利用料は、Organization settings → Billing から確認できます。
ちなみにデフォルトでは超過料金が発生しないようになっており、ストレージの料金が発生しそうになるとメールが来ます*6。
reg-suit
Cypress で得た画像の差分を reg-suit でGCSにアップロードしています。
reg-suit では比較対象を見つけるため、ブランチをトピックブランチにしてあげる必要があるので、 git checkout
しています*7。
if
失敗すると後続のステップは実行されないのですが、複数の落ちる箇所があるのに一度に気づきたいので、 if: always()
を入れまくっています。CircleCI で言うところの when: always
です。
また、各ステップごとに、実行条件を書けます。
CircleCI だと、if [[ $CIRCLE_BRANCH = master ]]; then
みたいに書く必要がありましたが、GitHub Actions では if: github.ref == 'refs/heads/master'
のように分かりやすく書けるようになりました。
GitHub Actions のイケてるところ
料金が安い
試算した結果うちの場合はユーザー数が多いこともあり、CircleCI よりもGitHub Actions のほうが安そうでした。そこで GitHub Actions に移行しました。
CircleCI の Performance Plan では、ユーザー数あたり15ドルの課金があります。うちインターン多いのでユーザー数ベースの課金があるとちょっと不利なのです。
GitHub Actions の場合は、CIにはユーザー数ベースの課金はありません*8。
使うツールが減らせる
うちはコードレビューもGitHub で行っているので、GitHub でCIも完結できるのは嬉しいです。使うツールを一つ減らせました。
公開されている GitHub Actions Workflow が多い
公式のものも、そうでないものも、すでにあるテンプレを組み合わせて書けるのは便利だなと思いました。
CircleCI にも似たような仕組み Orbs
がありますが、GitHub Actions ほど使われてないような気がします。
GitHub Actions の残念なところ
UI がイケてない
CircleCIのビルドの確認画面はSPAだったのでサクサク動くし、ログも割とリアルタイムに出ていました。 GitHub Actions はいちいちページ遷移が発生する上、ログの表示も遅くて設定ファイルを書くのはちょっとだるかったです。
Artifacts がzip になってしまう
いちいちzip解凍するのが面倒。 CircleCI みたいにHTMLページをホスティングしてくれる機能ほしかった。CircleCI の場合は認証まわりも面倒見てくれるのですごく使い勝手よかった。
まとめ
GitHub を使っている場合、CI を検討する際の有力な選択肢になりそうです。 UI やストレージ料金の面で課題は感じますが、「使うサービスを減らせる」という部分は大きな魅力に感じます。
今後の GitHub Actions の動きに期待しています!
*1:React Native のネイティブアプリのビルドはBitriseでやっているので今回の記事の範囲外となります
*2:目次のページ内リンクが機能しなくて使いにくい場合は、英語版をオススメ
*3:SocialDog ではGitHub Flow を採用しています。
*4:phpdbg メモリリークしてしまいなぜか動かないので、断念してxdebugを使っています。
*5:GitHub Actions は裏側Azureですし、Azureという選択肢もありますね
*6:これの構築をしているとき、Artifacts にかかる料金気づかずに、メール来ました。
*7:公式サイトにワークアラウンドな方法として紹介されていた
*8:すでにGitHub Teamsに支払っている場合