くりにっき

フルスタックキュアエンジニアです

個人gemのCIをほぼ全部Travis CIからGitHub Actionsに移行した

2日間で30個くらいのリポジトリGitHub Actionsに移行したのでメモ

tl;dr;

  • GitHub Actionsの並列数は魅力だがyamlの記述が多くなるので自分のように大量のgemをメンテしてなければTravis CIでよさそう

モチベーション

一番大きかったのは並列数の問題です。

毎年正月には 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並列というのが大きかったです。

sue445.hatenablog.com

GitHub Actions移行中に気づいたのですが、実際にはGitHubのプランによる並列数の上限もあるようでした。

https://help.github.com/ja/actions/automating-your-workflow-with-github-actions/about-github-actions#usage-limits

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.rubyRubyのバージョンで差異が出てくると思いますがほぼこのyamlコピペでいけると思います。 (Slack通知したいなら別途Secretsに SLACK_WEBHOOK の登録が必要)

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: Cache vendor/bundle
        uses: actions/cache@v1
        id: cache_gem
        with:
          path: vendor/bundle
          key: v1-gem-${{ runner.os }}-${{ matrix.ruby }}-${{ github.sha }}
          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

2020/1/4 0:40追記

${{ hashFiles('uuid.txt') }} って出来るだけ変わってほしいだけなら ${{ github.sha }} でいけないかなあ。

って指摘をもらったので修正

https://github.com/sue445/rubicure/pull/222/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 で見れば一目瞭然ですが新しいバージョンへの対応があまり積極的でないように見えます。

f:id:sue445:20200103195759p:plain

gemのようにマイナーバージョンくらいまで指定すればいい場合には気にしなくてもいいかもしれないですが、ウェブアプリケーションなどで本番環境とCIでパッチバージョンまで厳密に指定したい時には致命的です。

2つ目の理由は古いバージョンが突然消えることです。

GitHub ActionsのBetaが消えるちょっと前に actions/setup-ruby から2.3系が突然消えたことがあります。

github.com

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:mstshiwasakihttps://github.com/marketplace/actions/setup-rbenv を作ったというのを見ました。

mstshiwasaki.hatenablog.com

本家の https://github.com/rbenv/ruby-build を使ってれば古いRubyのバージョンも消えることはないし、matrixビルドでも使えたのでこれを本格採用しました。

setup-rbenvを使う場合の注意点

rbenv installRubyをビルドするのに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 を有効にするようにしました。

https://github.com/sue445/faker-precure/blob/6787351310e09b1c268c2c03b020fa7384a1b215/.github/workflows/test.yml#L105

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') }}

https://github.com/sue445/faker-precure/blob/6787351310e09b1c268c2c03b020fa7384a1b215/.github/workflows/test.yml#L13-L73

余談ですがsetup-rbenvを ubuntu-16:04 で使おうとした時にエラーになったのでPR投げてます

github.com

Gemfile.lockをコミットしないリポジトリでもキャッシュを保存したい

GitHub Actionsのキャッシュは同名のキャッシュキーで上書きできません。

これはCircleCIと同様の方式です。

sue445.hatenablog.com

通常は Gemfile.lockチェックサムをキーに含めればいいのですが、gemは通常 Gemfile.lock をコミットしないので困ります。

余談ですが最近の bundle gem だと Gemfile.lock をコミットするようになってますが、dependabotでバージョンアップするコストもあるので僕はgemだと常に依存gemの最新版を常にCIで使うようにしてます。(もしそれで問題ある場合はgemspecやGemfileで制御する)

そこでこれも力技ですが、keyv1-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') }}

https://github.com/sue445/faker-precure/blob/6787351310e09b1c268c2c03b020fa7384a1b215/.github/workflows/test.yml#L75-L86

GitHub Actionsの不満点

上に書いていない不満点など。

ジョブの手動リトライができない

不安定なテストがあってもCircleCIやTravis CIだとジョブの手動リトライができるのですが、GitHub Actionsだとそれがないので空コミットをpushして全部ジョブを実行しなおす必要があります。

.travis.yml に比べて記述が冗長になる

Travis CIだと割とよしなにビルドしてくれてたんですが、GitHub Actionsだとそのよしながないので全部自分で書く必要があります。

ケースバイケースだけど2〜3個しかgemを公開してない場合はTravis CIでいいのでは感はあります。

余談

進捗管理について

30個もあると自分でどれを対応したのか分からなくなるのでTrelloで進捗管理していました。

f:id:sue445:20200103194110p:plain

contributionsスクショ

右下の濃い3つが1/1〜1/3の分

f:id:sue445:20200103211342p:plain

f:id:sue445:20200103211451p:plain

f:id:sue445:20200103211503p:plain