2日間で30個くらいのリポジトリでGitHub Actionsに移行したのでメモ
tl;dr;
モチベーション
一番大きかったのは並列数の問題です。
毎年正月には https://github.com/sue445/rubicure/pull/187/files のような感じで全gemで .travis.yml
に新しいRubyのバージョンを追加する作業をしてるのですが、https://travis-ci.org/ は1 owner辺りの並列数が最大5ジョブの関係で30個以上のリポジトリで同時に .travis.yml
を編集するとそこでTravis CIが詰まるという問題がありました。
PRで .travis.yml
を修正してビルドが通ればマージするということをやってたのですが、このTravis待ちの関係で全リポジトリでバージョン上げ終わるのにだいたい半日くらいかかっていました。
ちなみに全gemでCIの設定を編集する作業は年に2〜3回発生しています。
今年もRuby 2.7対応するに辺りTravis待ちの懸念があったんで正月休みの機運でメンテしてないgem以外ほぼ全部GitHub Actionsに移行しました。
GitHub Actionsを選んだ理由
前にブログでも書いたのですが1リポジトリ辺り20並列というのが大きかったです。
GitHub Actions移行中に気づいたのですが、実際にはGitHubのプランによる並列数の上限もあるようでした。
The number of jobs you can run concurrently across all repositories in your account depends on your GitHub plan.
の下りです。
それでも現状のTravis CIよりは圧倒的に並列数が大きいのでGitHub Actionsのメリットは感じてます。
GitHub ActionsでgemのCIをするための設定
gemでよくある、複数のrubyのバージョンでrspecを流す設定はこんな感じです。
gemによって matrix.ruby
のRubyのバージョンで差異が出てくると思いますがほぼこのyamlコピペでいけると思います。
name: test on: push: schedule: - cron: "0 10 * * 5" # JST 19:00 (Fri) env: CI: "true" jobs: test: runs-on: ${{ matrix.runner }} strategy: fail-fast: false matrix: ruby: - 2.2.2 - 2.3.0 - 2.4.0 - 2.5.0 - 2.6.0 - 2.7.0 - 2.8.0-dev include: - ruby: 2.2.2 runner: ubuntu-16.04 - ruby: 2.3.0 runner: ubuntu-16.04 - ruby: 2.4.0 runner: ubuntu-latest - ruby: 2.5.0 runner: ubuntu-latest - ruby: 2.6.0 runner: ubuntu-latest - ruby: 2.7.0 runner: ubuntu-latest - ruby: 2.8.0-dev runner: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up rbenv uses: masa-iwasaki/setup-rbenv@1.1.0 - name: Cache RBENV_ROOT uses: actions/cache@v1 id: cache_rbenv with: path: ~/.rbenv/versions key: v1-rbenv-${{ runner.os }}-${{ matrix.ruby }} if: "!endsWith(matrix.ruby, '-dev')" - name: Reinstall libssl-dev run: | set -xe sudo apt-get remove -y libssl-dev sudo apt-get install -y libssl-dev=1.0.2g-1ubuntu4.15 if: matrix.runner == 'ubuntu-16.04' - name: Install Ruby run: | set -xe eval "$(rbenv init -)" rbenv install -s $RBENV_VERSION gem install bundler --no-document -v 1.17.3 || true env: RBENV_VERSION: ${{ matrix.ruby }} continue-on-error: ${{ endsWith(matrix.ruby, '-dev') }} - name: Generate unique cache key run: uuidgen > uuid.txt - name: Cache vendor/bundle uses: actions/cache@v1 id: cache_gem with: path: vendor/bundle key: v1-gem-${{ runner.os }}-${{ matrix.ruby }}-${{ hashFiles('uuid.txt') }} restore-keys: | v1-gem-${{ runner.os }}-${{ matrix.ruby }}- continue-on-error: ${{ endsWith(matrix.ruby, '-dev') }} - name: bundle update run: | set -xe eval "$(rbenv init -)" bundle config path vendor/bundle bundle update --jobs $(nproc) --retry 3 env: RBENV_VERSION: ${{ matrix.ruby }} continue-on-error: ${{ endsWith(matrix.ruby, '-dev') }} - name: Run test run: | set -xe eval "$(rbenv init -)" bundle exec rspec env: RBENV_VERSION: ${{ matrix.ruby }} continue-on-error: ${{ endsWith(matrix.ruby, '-dev') }} - name: Slack Notification (not success) uses: homoluctus/slatify@v2.0.0 if: "! success()" with: job_name: ${{ format('*build* ({0})', matrix.ruby) }} type: ${{ job.status }} icon_emoji: ":octocat:" url: ${{ secrets.SLACK_WEBHOOK }} token: ${{ secrets.GITHUB_TOKEN }} notify: needs: - test runs-on: ubuntu-latest steps: - name: Slack Notification (success) uses: homoluctus/slatify@v2.0.0 if: always() with: job_name: '*build*' type: ${{ job.status }} icon_emoji: ":octocat:" url: ${{ secrets.SLACK_WEBHOOK }} token: ${{ secrets.GITHUB_TOKEN }}
https://github.com/sue445/faker-precure/pull/19/files
以下、解説
weekly build
Travis CIだと Cron Jobs で毎週ビルドしていたので、同じことをGitHub Actionsでも実装しました。
on: push: schedule: - cron: "0 10 * * 5" # JST 19:00 (Fri)
CircleCIのscheduler同様UTC指定です。
公式のactions/setup-rubyではなくmasa-iwasaki/setup-rbenvを利用
GitHub Actions(Beta)時代は公式の actions/setup-ruby を使ってたのですが、下記のような不満がありました
1つ目は新しいRubyのバージョンへの対応が遅いことです。
https://github.com/actions/setup-ruby/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+ruby+support で見れば一目瞭然ですが新しいバージョンへの対応があまり積極的でないように見えます。
gemのようにマイナーバージョンくらいまで指定すればいい場合には気にしなくてもいいかもしれないですが、ウェブアプリケーションなどで本番環境とCIでパッチバージョンまで厳密に指定したい時には致命的です。
2つ目の理由は古いバージョンが突然消えることです。
GitHub ActionsのBetaが消えるちょっと前に actions/setup-ruby から2.3系が突然消えたことがあります。
2.3系はEOLを過ぎているので仕方ないかもしれないですが、外部要因で突然全てのCIがコケるのは割とつらいです。
以上のようなことがあって actions/setup-ruby への信頼度が自分の中でなくなったので別の手法を選びました。
ボツ案:Dockerイメージのrubyを使う
GitHub ActionsではDockerイメージが使えるのでこれが自分の中で結構有力だったのですが、下記のようにmatrixで使えなかったのでボツになりました。
jobs: test: runs-on: ubuntu-latest strategy: fail-fast: false matrix: image: - ruby:2.2.2 - ruby:2.3 - ruby:2.4 - ruby:2.5 - ruby:2.6 - ruby:2.7 - rubylang/ruby:master-nightly-bionic steps: - uses: actions/checkout@v2 - name: Set up ruby uses: docker://${{ matrix.image }}
https://github.com/sue445/rubicure/pull/217/files
実際のエラー
### ERRORED 16:31:48Z - Your workflow file was invalid: The pipeline is not valid. .github/workflows/test.yml (Line: 33, Col: 15): Unrecognized named-value: 'matrix'. Located at position 1 within expression: matrix.image
https://github.com/sue445/rubicure/runs/369212602
https://github.com/sue445/plant_erd/blob/v0.1.1/.github/workflows/test.yml#L27 のように services
で使うDockerイメージの引数だと変数が使えたので、GitHub Actionsのyamlは変数が使える場所と使えない場所があるっぽいです。
masa-iwasaki/setup-rbenvを利用
去年の年末にSlackの ruby-jpワークスペース で id:mstshiwasaki が https://github.com/marketplace/actions/setup-rbenv を作ったというのを見ました。
本家の https://github.com/rbenv/ruby-build を使ってれば古いRubyのバージョンも消えることはないし、matrixビルドでも使えたのでこれを本格採用しました。
setup-rbenvを使う場合の注意点
rbenv install
でRubyをビルドするのに4〜5分かかるのでキャッシュ必須です。
setup-rbenvのREADMEだと /home/runner/.rbenv
を丸ごとキャッシュしてますが、rbenvやruby-buildのgitリポジトリもキャッシュに含めるとキャッシュが肥大化しそうなので*1 ~/.rbenv/versions
のみキャッシュにしました。
- name: Cache RBENV_ROOT uses: actions/cache@v1 id: cache_rbenv with: path: ~/.rbenv/versions key: v1-rbenv-${{ runner.os }}-${{ matrix.ruby }} if: "!endsWith(matrix.ruby, '-dev')"
細かいですが 2.8-dev
のように開発版のrubyはキャッシュさせずに毎回ビルドをしたいので if: "!endsWith(matrix.ruby, '-dev')"
をつけてます
Travis CIのallow_failuresをGitHub Actionsでも実現する
Travis CIだと ruby-head
(開発版のRuby)も常にビルドしてdeprecation warningがないかを確認してましたが、それをGitHub Actionsでどうするか悩みました。
割と力技ですが continue-on-error: ${{ endsWith(matrix.ruby, '-dev') }}
のようにして、-dev
がついてるバージョンの時は continue-on-error
*2 を有効にするようにしました。
GitHub ActionsでRuby 2.3以下をビルドする
ubuntu-latest
( ubuntu-18.04
) だとopensslのバージョンの関係でRuby 2.3以下のビルドができません。
+ rbenv install -s 2.3.0 Downloading ruby-2.3.0.tar.bz2... -> https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.0.tar.bz2 Installing ruby-2.3.0... BUILD FAILED (Ubuntu 16.04 using ruby-build 20191225-1-gbac1f1c) Inspect or clean up the working tree at /tmp/ruby-build.20191231153709.7115.LMQ9du Results logged to /tmp/ruby-build.20191231153709.7115.log Last 10 log lines: make[2]: Leaving directory '/tmp/ruby-build.20191231153709.7115.LMQ9du/ruby-2.3.0/ext/openssl' exts.mk:206: recipe for target 'ext/openssl/all' failed make[1]: *** [ext/openssl/all] Error 2 make[1]: *** Waiting for unfinished jobs.... installing default nkf libraries linking shared-object nkf.so make[2]: Leaving directory '/tmp/ruby-build.20191231153709.7115.LMQ9du/ruby-2.3.0/ext/nkf' make[1]: Leaving directory '/tmp/ruby-build.20191231153709.7115.LMQ9du/ruby-2.3.0' uncommon.mk:203: recipe for target 'build-ext' failed make: *** [build-ext] Error 2 ##[error]Process completed with exit code 1.
https://github.com/sue445/rubicure/runs/369164075
これも力技ですが、2.3以下では ubuntu-16.04
を使いつつデフォで入ってる libssl-dev
を削除して libssl-dev=1.0.2g-1ubuntu4.15
を入れ直しています。( ubuntu-18.04
だと 1.0.2g-1ubuntu4.15
が見つからなくてインストールできない)
runs-on: ${{ matrix.runner }} strategy: fail-fast: false matrix: ruby: - 2.2.2 - 2.3.0 - 2.4.0 - 2.5.0 - 2.6.0 - 2.7.0 - 2.8.0-dev include: - ruby: 2.2.2 runner: ubuntu-16.04 - ruby: 2.3.0 runner: ubuntu-16.04 - ruby: 2.4.0 runner: ubuntu-latest - ruby: 2.5.0 runner: ubuntu-latest - ruby: 2.6.0 runner: ubuntu-latest - ruby: 2.7.0 runner: ubuntu-latest - ruby: 2.8.0-dev runner: ubuntu-latest steps: # 略 - name: Reinstall libssl-dev run: | set -xe sudo apt-get remove -y libssl-dev sudo apt-get install -y libssl-dev=1.0.2g-1ubuntu4.15 if: matrix.runner == 'ubuntu-16.04' - name: Install Ruby run: | set -xe eval "$(rbenv init -)" rbenv install -s $RBENV_VERSION gem install bundler --no-document -v 1.17.3 || true env: RBENV_VERSION: ${{ matrix.ruby }} continue-on-error: ${{ endsWith(matrix.ruby, '-dev') }}
余談ですがsetup-rbenvを ubuntu-16:04
で使おうとした時にエラーになったのでPR投げてます
Gemfile.lockをコミットしないリポジトリでもキャッシュを保存したい
GitHub Actionsのキャッシュは同名のキャッシュキーで上書きできません。
これはCircleCIと同様の方式です。
通常は Gemfile.lock
のチェックサムをキーに含めればいいのですが、gemは通常 Gemfile.lock
をコミットしないので困ります。
余談ですが最近の bundle gem
だと Gemfile.lock
をコミットするようになってますが、dependabotでバージョンアップするコストもあるので僕はgemだと常に依存gemの最新版を常にCIで使うようにしてます。(もしそれで問題ある場合はgemspecやGemfileで制御する)
そこでこれも力技ですが、key
で v1-gem-${{ runner.os }}-${{ matrix.ruby }}-${{ hashFiles('uuid.txt') }}
のようにUUIDをキャッシュのプライマリキーに含めつつ、restore-keys
(セカンダリ)の方で v1-gem-${{ runner.os }}-${{ matrix.ruby }}-
のようにして取得するようにしています。( https://help.github.com/ja/actions/automating-your-workflow-with-github-actions/caching-dependencies-to-speed-up-workflows#matching-a-cache-key にあるように restore-keys
で複数マッチした場合は最新のキャッシュが使われる)
- name: Generate unique cache key run: uuidgen > uuid.txt - name: Cache vendor/bundle uses: actions/cache@v1 id: cache_gem with: path: vendor/bundle key: v1-gem-${{ runner.os }}-${{ matrix.ruby }}-${{ hashFiles('uuid.txt') }} restore-keys: | v1-gem-${{ runner.os }}-${{ matrix.ruby }}- continue-on-error: ${{ endsWith(matrix.ruby, '-dev') }}
GitHub Actionsの不満点
上に書いていない不満点など。
ジョブの手動リトライができない
不安定なテストがあってもCircleCIやTravis CIだとジョブの手動リトライができるのですが、GitHub Actionsだとそれがないので空コミットをpushして全部ジョブを実行しなおす必要があります。
.travis.yml に比べて記述が冗長になる
Travis CIだと割とよしなにビルドしてくれてたんですが、GitHub Actionsだとそのよしながないので全部自分で書く必要があります。
ケースバイケースだけど2〜3個しかgemを公開してない場合はTravis CIでいいのでは感はあります。
余談
進捗管理について
30個もあると自分でどれを対応したのか分からなくなるのでTrelloで進捗管理していました。
contributionsスクショ
右下の濃い3つが1/1〜1/3の分
*1: https://help.github.com/ja/actions/automating-your-workflow-with-github-actions/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy にもキャッシュサイズの制限がある
*2:https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idstepscontinue-on-error