Scala クロスビルド用バージョンを一元管理する GitHub Action を作りました
この記事は Scala Advent Calendar 2021 7日目の記事です。
sbt で定義した crossScalaVersions
と、GitHub Actions のビルドマトリクス matrix.scala
を一元管理したくて GitHub Action を作成したので、紹介します。
sbt の crossScalaVersions を使ったクロスビルド
ライブラリやツールなどを実装していると、異なる Scala バージョンをサポートしたくなることがあります。
コードベースを異なるバージョンに対してビルドすることをクロスビルドなどと言います。
sbt でのクロスビルドは、crossScalaVersions
を用いて次のように複数の Scala バージョンを宣言します。
crossScalaVersions := Seq("3.1.0", "2.13.7", "2.12.14", "2.11.11")
scalaVersion := crossScalaVersions.value.head
このように宣言しておけば、sbt ++ test
などとコマンドを実行すると、各バージョンに対して同じコマンドを実行できます。
++
は使用する Scala バージョンを一時的に切り替える命令で、次のように使えます。
-
++ compile
: 宣言されたすべてのバージョンでcompile
などのコマンドを実行 -
++ 2.13.5
:2.13.5
など特定のバージョンに切替 -
++ 2.13.5 compile
:2.13.5
に切り替えて、compile
などのコマンドを実行
GitHub Actions でのクロスビルドと CI
コードに何らかの変更を加えたときは、サポートしたいすべての Scala バージョンでちゃんとビルドできることを CI で確認したいですね。
GitHub Actions で単純にやるとこんな CI ジョブになるでしょう。
さきほど説明した ++
により、全部のバージョンでテストが実行されます。
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: sbt ++ test # 全部のバージョンをテスト
ただ、この方法には次のような手間があります。
- 全バージョンが1つのジョブで実行されるので、どのバージョンで失敗したかが分かりにくいい。ログを見る必要がある。
- どれかひとつが失敗するとそこで打ち切り(フェイルファスト)なので、まだ実行してないバージョンが成功するかどうかが分からない。失敗したバージョンに対して修正したあと、また全体を再実行する必要がある。
ビルドを制御する変数が Scala バージョンひとつだけなら、これでもいいかもしれません。
ですが、Java のバージョン、Node.js のバージョン、Akka のバージョンなど、ビルド変数の組み合わせが増えると大変なことになります。
こうしたビルド変数の組み合わせを扱いやすくするために、メジャーな CI ツールはビルドマトリクスをサポートしています。
GitHub Actions もビルドマトリクスをサポートしています。
ビルドマトリクスを使うことで、Scala のクロスビルドは次のように書けます。
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false # どれかが失敗しても打ち切らずに全体を実行し
matrix:
# この変数名はユーザーが自由に定義可能
scala: [3.1.0, 2.13.7, 2.12.14, 2.11.11]
steps:
- uses: actions/checkout@v2
- run: sbt ++${{ matrix.scala }} test # マトリクスの変数を利用
これでメデタシメデタシ……としたいのですが、これには少し面倒があります。
散らばったバージョン指定をなんとかしたい
ビルドマトリクスの利用により、Scala のバージョン宣言が sbt と CI の2箇所に重複してしまいました。
crossScalaVersions := Seq("3.1.0", "2.13.7", "2.12.14", "2.11.11")
matrix:
scala: [3.1.0, 2.13.7, 2.12.14, 2.11.11]
サポートするバージョンをめったに更新しないのならこれでもいいのですが、Scala はたいてい、年に数回はマイナーバージョンがリリースされ、更新したくなります。
リリースのたびに手でバージョンを修正していると、「sbt の crossScalaVersions
は修正したが、GitHub Actions の matrix
を修正し忘れてしまった」ということが置きるものです。
ぼくも何度かやりました(まあ、大事にはならないのですが)。
こうした細かいトイルを放置していると 「バージョンアップするぞ!」 という気持ちが萎えてしまいませんか?
なんとかして、バージョン指定を一元管理したいですね。
TravisCI 時代の Scala クロスビルド
2020年まで、Scala OSS コミュニティでは CI の老舗 TravisCI がデファクトスタンダードでした。
TravisCI を使った Scala プロジェクトのために、バージョン指定を一元管理できるツールが作られたほどです。
このツールは、TravisCI の .travis.yml
に書かれた Scala バージョン番号をパースし、sbt から使えるようにする(つまり crossScalaVersions
のバージョン指定を省略できる)、というものです。
ぼくも自分が管理する OSS でも TravisCI と sbt-travisci には長年お世話になりました。
ですが、2020年に TravisCI の無料使用枠は制限がきつくなり、まったく足りなくなってしまいました。
また GitHub Actions や CircleCI など比較的最近の CI ではよくあるジョブを再利用できますが、黎明期からあって枯れていた TravisCI ではそれができず、今後の利便性への不安を感じたのもあります。
そんなわけで、ぼくは TravisCI から GitHub Actions に移行しました。
こうした動きは Scala コミュニティに広く見られました
GitHub Actions 時代になって困ったこと
ところが GitHub Actions には sbt-travisci 相当のものはありません。
ぼく自身 sbt-travisci 相当のツールを GitHub Actions 用に作ろうとして、「これは作るの面倒だぞ」と気づきました。
TravisCI での CI ジョブ定義は、機能も少なく非常に簡素でした。
.travis.yml
に定義された scala
というリストを調べれば、カンタンに Scala のバージョンを特定できました。
一方、GitHub Actions では非常に柔軟に CI ジョブを定義できます。
ファイル名も自由(ci.yaml
、build.yaml
……)ですし、ひとつのファイルの中に複数のジョブを宣言できます。
また matrix
名もユーザーが定義できるので、scala
でも scala212
でも scala_ver
でもなんでもよいのです。
つまり、モダンな CI 定義の柔軟さゆえに、どこに宣言された Scala バージョンを「正」とするか非常に厄介なのです。
そこで cross-scala-versions を作った
sbt-travisci とはアプローチを逆転させることにしました。
従来のアプローチ sbt-travisci |
今回のアプローチ cross-scala-versions |
|
---|---|---|
一元管理先 | CI | sbt crossScalaVersions
|
参照側 | sbt crossScalaVersions
|
CI |
そのアプローチで作ったのが cross-scala-versions という GitHub Action です。
使い方はこんな感じです。
jobs:
get_scala_versions:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: exoego/cross-scala-versions@master
id: run
outputs:
scala_versions: ${{ steps.run.outputs.scala_versions }}
build:
needs: get_scala_versions
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
scala: ${{ fromJson(needs.get_scala_versions.outputs.scala_versions) }}
steps:
- uses: actions/checkout@v2
- run: sbt ++${{ matrix.scala }} test
流れとしては、
-
get_scala_versions
というジョブで拙作のcross-scala-versions
を呼び出し、sbt のcrossScalaVersions
に宣言されたバージョンを JSON 配列形式で取得します。 - それを、後続のメインジョブ
build
から参照し、matrix.scala
にセットしています。
GitHub Actions のさまざまな機能を組み合わせる都合で少し記述は多くなってしまいました。
ただ一度宣言してしまえばあとは crossScalaVersions
をメンテナンスするだけ!!
バージョンの一元管理という目標は達成できました!!
cross-scala-versions のまだイケていないところ
- 昨日 2021/12/06 に作ったばかりなので、本当に単機能で、まだおかしいところもあるかもしれません。
-
crossScalaVersions
解析のために Docker で JVM を立ち上げて sbt を実行するので、20秒前後かかります。- クロスビルドするときは遅いジョブだと数百秒かかるので、まあ許容範囲なのではないかと思います。
- GitHub Actions の記述ももっと簡潔にできないか…?
もっといいアイデアがありましたら、ぜひ issues をオープンしたり、プルリクエストを送るなどお待ちしております!!
関連するツールの紹介
sbt から GitHub Actions の YAMLファイルを生成する、というアプローチもあります。
Scala の Typelevel コミュニティや Disney Streaming 社でご活躍されている Daniel Spiewak さん中心に開発されています。
ぼくにとってはちょっと機能が過剰で、移行作業も大変なので、まだ使ってはいません。
これから新規開発するなど、ユースケースによっては役立つ場面が多いことでしょう。
Discussion