Kengo's blog

Technical articles about original projects, JVM, Static Analysis and JavaScript.

Gradle用のGitHub Actions勘どころ

GitHub Actions のベータ版が個人リポジトリの方に来ているので、色々と試しています。使っているテンプレートプロジェクト実アプリケーションプロジェクトから、いくつか事例を紹介します。

なお各種単語の定義が公式サイトにあるので、いちど目を通すことをおすすめします。

テストレポートをworkflow runに添付する

upload-artifact actionを使うことで、テストレポートをworkflow runに対して添付することができます。

    - name: Build with Gradle
      run: ./gradlew build
    - name: Upload Test Report
      uses: actions/upload-artifact@v1
      if: always()
      with:
        name: test results
        path: build/test-results/test

if: always() がミソで、これがないとテスト失敗時にupload-artifactが実行されません。always()の説明は公式サイトにあります。

複数のjobを並列に実行する場合は、nameを固有かつ直感的な名前にしないと、レポートをダウンロードする際にどれを確認すべきかわからなくなってしまいますので要注意です。

wrapperとDockerコンテナどちらを使うべきか

Gradleを使ったビルドは、大きく分けて2つの方法が採れます。 Jobを実行しているマシンにJDKをインストールする方法と、Docker Hubで公開されているgradleのイメージを使う方法です。 なおGradle用の非公式actionが存在しますが、特にメリットがないため検討していません。

Jobを実行しているマシンにJDKをインストールしてGradle Wrapperを叩くケースが最もシンプルと言えます。 Wrapperを使うため、開発者が手元で使うGradleとバージョンを合わせることも容易です(Wrapperを使わずGradleをインストールしてPATHに通しても良いが特にメリットがない)。

    steps:
    - uses: actions/checkout@v1
    - name: Set up JDK 11
      uses: actions/setup-java@v1
      with:
        java-version: 11
    - name: Build with Gradle
      run: ./gradlew build

Docker Hubのgradleイメージを使う場合は、with.argsを使って実行するコマンドを指定します。 e2eテストのようなコマンドに依存するビルドでは使いにくい面もあると考えられますが、持ち運びの効くコンテナでCIを回せるのは嬉しい点です。YAMLファイルの見通しも比較的良いです。

    steps:
    - uses: actions/checkout@v1
    - name: Build with Gradle
      uses: docker://gradle:5.6-jdk11
      with:
        args: gradle build

なお今のところ、この2つの方法には速度の違いはないようです。Dockerコンテナを使うとsetup-javaがなくなりGradleインストールの時間が短縮される反面、イメージをpullする時間がかかります。Dockerイメージやローカルファイルシステムのキャッシュが導入されたら、このあたりは変わってくるかもしれませんね。

Maven Remote Repositoryは何が良いか

Organizationで使う場合はActionsを実行するリージョンが選べるそうです。

が、少なくとも私の個人リポジトリでは選択できない状態です。のでネットワーク的にどこが近いのか確信はないのですが、いくつか実行して試してみました:

試行回数を重ねないと確かなことは言えませんが、少なくともMavenリポジトリの物理位置がビルド速度に影響出るのは間違いありません。自前でRemote Repositoryを運用している場合は、それを物理的にどこに置くか考える必要がありそうです。あるいはpackage-registryを使っても良いかもしれませんね。

SonarCloudを使う

これは至ってシンプルで、sonar.host.url, sonar.organizationそしてsonar.projectKeyシステムプロパティで指定するだけです。公式フォーラムで紹介されています。これだけで通常の解析もブランチ解析PR解析もいけます。

    - name: Build with Gradle
      run: ./gradlew sonarqube -Dsonar.organization=foo -Dsonar.projectKey=bar -Dsonar.host.url=https://sonarcloud.io
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        SONAR_TOKEN: ${{ secrets.SONARCLOUD_TOKEN }}

上記の例では -D オプションを使って指定していますが、build.gradleに直接書いても大丈夫です。 環境変数として GITHUB_TOKENSONAR_TOKENの設定が必要です。

READMEにステータスバッジを埋める

公式ドキュメントが公開されましたので、それを参照すればOKです。

~/.gradle をキャッシュしたい!

GradleやMavenを使っていると、Mavenリモートリポジトリからダウンロードしたjarファイルをキャッシュしたくなります。残念ながら現時点ではその方法は無いようです。

Actions toolkitに入っているtool-cacheを使って~/.gradle/cachesをキャッシュするActionsを書いてみたのですが、キャッシュの保存はできてもキャッシュされたディレクトリをfindできません。Debugログを有効化しても特に情報は得られませんでした。

Javaに限らず他の言語のコミュニティにも期待されている機能だと思いますので、今後に期待しています。

selenium-jupiterを試した

JUnit 5にTemporalyFolderに相当する機能がなかったため、自分のプロジェクトでは長らくJUnit4を使っていました。しかし5.4でTempDirが来たため、徐々にJUnit5に置き換えています。

その過程でSelenideを使った統合テストをJUnit5に置き換えたのですが、selenium-jupiterがなかなか便利でした。作業したリポジトリこちらです。 ただ一筋縄に行かない部分もあるので、備忘録を残します:

SpringBootTestとSelenideとの噛みあわせ

SpringBootTestにはランダムなポートでアプリケーションサーバを起動する機能がありテストの並列実行に役立ちます。 またselenium-jupiterはSelenideをサポートしており、テストメソッドのパラメータに SelenideDriver を指定するとインスタンスを作って注入してくれます。

さてSelenideにはbaseUrlという設定がありConfiguration.baseUrlにURL文字列を指定しておくとSelenideDriverインスタンス作成時に使ってくれます。 これによりテストメソッドではスキーマドメイン名、ポート番号などを省略した相対URLを指定するだけで済むのですが。SelenideDriverインスタンス@BeforeEachメソッドが実行されたタイミングで既に作成されているので以下のようにbaseUrlを指定しても効いてくれません。

@LocalServerPort
private int port;

@BeforeEach
void config() {
  Configuration.baseUrl = String.format("http://localhost:%d/", port);
}

@Test
void test(SelenideDriver driver) {
  System.out.println(driver.config().baseUrl()); // デフォルトの http://localhost:8080/ になってしまう
}

現状きれいに解決する方法が見当たらないので、テストメソッドでは相対URLではなく絶対URLを使うようにしています。

SpringBootTestするならDocker内ブラウザは使わないほうが良い

selenium-jupiterではDocker内のブラウザを使うこともできるのですが、ドキュメント末尾に注意書きがあるようにローカルにアプリケーションサーバを建てているときは一筋縄では行きません。DockerコンテナからDockerホスト(localhost)にHTTP接続をする必要があるのですが、ホスト名を解決する統一的な手法が存在しないのです。特にLinux環境ではホスト名ではなくIPアドレスを取得する必要があり、一度 ip addr show docker0|grep 'inet '|awk '{$1=$1};1'|cut -d ' ' -f 2|cut -d / -f 1 などの操作をしてシステムプロパティ経由でテストに対して渡す必要があり面倒です。baseUrlが使えればホスト名構築処理を抽象クラスで集中管理できたのでまだマシだったのでしょうが……。

他にもブラウザで何らかの問題が起こったときのトラブルシュートが面倒ということもあり、SpringBootTestを使う場合はDocker内ブラウザは使用しないほうが良いのではと感じました。Travis CIなら安定版のChromeをCI環境にインストールする方法があるので、コンテナを使う必要性もあまりありません。

Gradleプロジェクト用semantic-releaseプラグインを書いた

Gradle用のsemantic-releaseプラグインJavaでsemantic-releaseを再実装したものしか無かったので、TypeScriptで書いたものを作りました。

github.com

./gradlew publish を叩くだけのシンプルなものですが、CI周りの設定はGradle側に寄せてあるはずなのでこれで問題なく運用できます。バージョンをgradle.propertiesで管理する必要がある点にのみ注意してください。

Java屋がsemantic-releaseに思うこと

最近Java周りでもsemantic-releaseの利用機会が増えています。Gradle pluginMaven pluginが生まれ、特に後者はyarn*1で実行されるため既存のプラグインとも組み合わせやすく、JavaScriptと比較しても遜色ない状態と言えそうです。

2019年3月時点で、Java特にMavenがどのようにsemantic-releaseを活用できるのか、まとめてみます。

semantic-releaseとは

プロジェクトにおいて以下の制約を導入することで、リリース作業をより一段階自動化する仕組みです。

  1. Semantic Versioningを使ったバージョン番号の付け方
  2. Conventional Commit Messagesを使ったコミットコメントの書き方

すでにJavaコミュニティにおいてもSemantic Versioningは標準となっているため、実際に学ばなければならないのはSemantic Commit Messageだけと言えます。

semantic-releaseは具体的なリリース作業を定義しません。このため使っているプロジェクト管理ツールやCIサービスを問わずに利用可能です。代わりに、リリース手順を構成するステップを定義しており、利用者はステップごとに必要な処理をフックできます。 これは、Mavenがビルドライフサイクルを構成するフェーズを定義しているのと近いです。Maven自身が各フェーズにおける具体的な処理を定義せずプラグインに委ねているように、semantic-releaseも具体的なリリース作業の定義をプラグインに委ねています。

各Stepの説明はsemantic-releaseのREADME.mdに書かれています:

Step名 説明
Verify Conditions リリースに必要な条件がすべて揃っていることを確認する
Get last release Git tagsから最新(前回)のリリースバージョンを取得する
Analyze commits 最新(前回)のリリースから今回のリリースまでに含まれるすべてのコミットを解析する
Verify release リリース可能な状態かどうか検証する
Generate notes 最新(前回)のリリースから今回のリリースまでに含まれるすべてのコミットからリリースノートを生成する
Create Git tag 新しくGitタグを作成する
Prepare リリースの準備をする
Publish リリースを出荷する
Notify 新しく作成したリリース、または発生したエラーについて通知する

GitHub Actionsを使えば、PR後にPrepare Stepまで処理を進め、承認を受けたらPublish & Notifyするという運用もできそうです。出荷承認プロセスを持つコミュニティでも活用できるでしょう。

Mavenとの併用

semantic-releaseをMavenと併用する場合、semantic-releaseがMavenを実行する形になります。Publish Stepでmvn deployする形です。

package.jsonは必須ではないですが、実際にはこのように作成してしまったほうがプロジェクトの見通しは良くなると思われます。これはpom.xmlをコミットするために@semantic-release/gitのカスタマイズが必要となるためです。またdevDependenciesにsemantic-releaseとそのプラグインが列記されるため、それらのバージョン管理がしやすくなる利点もあります。 プロジェクトにpom.xmlpackage.jsonが同居するので、CIツール側で設定ファイルの有無によるビルドツールの推定をしている場合は注意が必要かもしれません。

高速なリリースが行えるのが大きな利点

semantic-release最大の利点は、なんと言っても頻繁にリリースが行えることです。何も考えずPull Requestをマージするだけで、すべてのリリース作業が実施されMaven Cental等にデプロイを行うことができます。LeanやContinuous Deliveryが証明したように、高速かつ頻繁にユーザへ変更を届けることはプロジェクトの成功へダイレクトに効きます。導入可能なプロジェクトでは速やかに入れていくことが望ましいと思います。

他には、semantic-releaseを利用することでリリースマネージャの手作業、例えばタグ打ちやCHANGELOG作成を自動化できる点も嬉しいです。GitHub Releaseの更新のような従来から別の自動化手法があったものも、コミットコメントをSingle Sourceとして利用する手法に切り替えることで、透明性と正確性を確保できます。

考えられるデメリットはそこまで大きくない

一般に、頻繁なリリースはユーザに追随するための負担を強いる可能性がありますが、semantic-releaseはバージョン番号をSemantic Versionに従った形で決めるので、ユーザもバージョン更新の必要性と負担を簡単に見積ることができます。デプロイ回数が増えるのでMaven Repositoryの記憶容量が気になるかもしれませんが、メリットに比較して十分に安いコストではと感じます。OSS開発においてはここは特に気にならない問題でしょう。

Semantic Commit Messageをチームに普及するのが最初の難関か

デメリットが小さいならJavaコミュニティにもすぐに普及するでしょうか?個人的には懐疑的です。大きく分けて2つの問題があります。

まずSemantic Commit Message自体、そこまでクリアに定義されているものではありません。AngularJS Commit Message Conventionsから生まれたもので、多様なプロジェクトに適合できるものにはまだなっていません。

またSemantic Commit Messageに定義されていないコミット種別を利用することも禁止されていません。プロジェクトごとにコミット種別の亜種が生まれ、収集つかなくなる可能性がありそうです。
例えば依存ライブラリのバージョンアップはchore, build, fix, securityのいずれにするべきでしょう?実際にProbotプロジェクトではchore(package), chore(dependencies), build(package), fix(package)のすべてが使われています。1プロジェクト内でも統一されていないわけです。
今のところ周辺ツールもあまり賢くないので、PRレビュー時にレビュアーがコミット種別を確認する必要があります。プロジェクト導入時の最初の難関は、不明瞭な定義をローカルルールに落とし込んできちんと普及させる、このプロセスにあるでしょう。

次に、Semantic Commit Messageの利用を忘れたときに、コミットコメントの修正が面倒という問題があります。Gitはこれをサポートしていますが、コミット履歴の修正は基本的な操作と比べて難度が高いため、ある程度の慣れが必要です。 これについてはPRをSquash Commitしてしまうという、若干粗い解決方法があります。GitHubはSquashしてマージする時のコミットコメントをPRタイトルから取るので、PRタイトルをSemantic Commit Messageに準じたものにすればよいのです。PRタイトルはWeb画面から簡単に修正できるので、慣れていない人でも回しやすいでしょう。なおSemantic Commit Probotはこの運用を想定した実装になっていて、コミットコメントがSemantic Commit Messageとして不正な場合でもPRタイトルが問題なければパスしてくれます。

機能は十分なので運用経験と実績を積むべき時期

以上で、Java屋がsemantic-releaseに思うことを見てきました。

機能的には充分で、すぐにプロジェクトに適用することができます。nexus-staging-maven-pluginmaven-release-pluginなどの既存設定をそのまま使えるので、新規プロジェクトのみならず既存プロジェクトへの導入も簡単でしょう。

まだJavaコミュニティにおいてはさほど知名度がないため、コミュニティでの開発に浸透するのはまだ先だと思いますが、個人プロジェクトではぜひ使ってほしいです。ユーザが増え実績が積まれれば、Javaコミュニティにおいても利用が進むことでしょう。

*1:npmでも良いはずだが、semantic-release-pluginのドキュメントはyarnで統一されているので、長いものには巻かれておく

2019年初頭でのMaven Site作成における注意点

最近新しいMaven Plugin用サイトを作ったので、mvn siteで作成できるサイトについて現時点での注意点をまとめる。

skinについて

maven-fluido-skinが最も使われていてかつ実用的と思われる。以下の記述はこのskinのv1.7を利用することを前提とする。

maven-site-pluginの設定方法

Maven3から<reportPlugins>による設定が可能になっているが、最新の公式サイトでは非推奨になっているので利用を避ける。

This new configuration format is not actually ready for end-users: please don't use it for the moment.

Copyright情報の生成

ページのフッターにCopyright情報が記載されるが、これはpom.xml<organization><inceptionYear>から情報を得て生成される。データが得られないとCopyright Holderが空になり違和感があるので、記入しておくと良い。

Markdownの利用

maven-site-plugindoxia-module-markdownに対する依存を追加すれば良い。.mdファイルはsrc/site/markdown以下に置く。

        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-site-plugin</artifactId>
          <version>3.7.1</version>
          <dependencies>
            <dependency>
              <groupId>org.apache.maven.doxia</groupId>
              <artifactId>doxia-module-markdown</artifactId>
              <version>1.8</version>
            </dependency>
          </dependencies>
        </plugin>

Maven Plugin用サイトの生成

Requirement 情報の生成

plugin-info.htmlMavenJDK 1.8、MemoryそしてDisk SpaceのRequirementを明記することができる。これはmaven-plugin-pluginの機能で、<requirements>を利用して設定できる。他にも特殊なRequirementがあるなら<others>を使って明記することもできるようだ。

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-plugin-plugin</artifactId>
        <configuration>
          <requirements>
            <maven>${maven.version}</maven>
            <jdk>1.8</jdk>
          </requirements>
        </configuration>
      </plugin>

HelpMojoの作成

昔から変わらないのでコピペで良い。

2018年のOSS活動状況まとめ

GitHubによると、今年は合計950 Contributions(25日時点)でした。仕事でほぼ使ってないのにこの数字というのは、多いのでしょうか少ないのでしょうか。

SpotBugs周りの開発

2018年もSpotBugsが一番活発な開発でした。新機能開発はほぼ無く純粋なバグ修正がほとんどですが、リリース周りも担っていたので100 commitsを超える貢献をしていました。

f:id:eller:20181225115534p:plain
contributors of spotbugs 2018

同organizationの他プロジェクトでもsonar-findbugs 48 commits, spotbugs-gradle-plugin 43 commits, spotbugs-archetype 17 commitsという感じで、Mavenプラグインを除けば最も活発なcontributorでした。

個人プロジェクト

個人プロジェクトではrtd-bot 72 commits, errorprone-slf4j 46 commits, findbugs-slf4j 44 commits, gradle-helper-for-java-8 40 commits, javadocky 39 commits, what-is-maven 13 commitsという感じでした。

rtd-botで使っているGitHub Probotはいろんな自動化が気軽にできそうで楽しいです。Sentryでサービスの監視をしていますが、Heroku上でChromeが起動しないことがあり、サービス自体はあまり安定させられていません。コストを掛けてサーバのスペックを上げないと解決でき無さそうなので、Read The Doc側のAPIが整備されるのを待つ形です。

今年でErrorproneプラグインもだいたい書けるようになり、これでPMD・Checkstyle・SpotBugs(FindBugs)・ErrorproneとJava周りの静的解析ツールはひととおりプラグインを実装できるようになりました。最近はJenkinsやMavenプラグインに加えGradleのプラグインも学習中なので、ビルド周りの自由度はだいぶ上がった気がします。

個人的に気になっているProject Reactorについては、Javadocky以外に使いみちを見いだせていないのが残念な感じです。流行りのサーバーレスと合わせるならSpring Cloud Functionのような使いみちになるのでしょうが、そもそもサーバーレスではSpring自体ほぼ使わないという……。GraalVMでの起動高速化が一般的になれば別でしょうが、それまではJavaを利用しないという選択肢も有力だと個人的には考えています。

来年の抱負

SpotBugs本体は旧バージョンのメンテナンスを止めて新機能開発に注力できる予定なので、以前からやりたいと話しているマルチスレッド対応を盛り込めればと思っています。1年でできるとは正直思ってませんが、とりあえず依存関係を整理してスレッドアンセーフなクラスを特定することはできるでしょう。おそらくBCELに依存するほぼすべてのDetectorがダメだとは思いますが……。

加えるなら、TypeScript・Kotlin・Rustといった言語の開拓でしょうか。基礎はもうあると思いますし、簡単なツールなら実装済みなのですが、もう少し複雑な事例も経験しておきたいところです。

Read The Docs利用者用にPRレビュー支援Probotを作りました

私がSpotBugsなどで使っているドキュメントホスティングサービス、Read The Docsを使う上で助けになるGitHub Probotを作成しました。

github.com

ドキュメントがいつもどおり全部英語なので、日本語での解説をここに書いておきます。

想定ユーザ

  • Read The Docsを使ったドキュメント作成を複数人で行っている
  • Read The Docsの設定をあまりいじっていない

解決する問題

  • Read The Docsが単体でPRレビュー用ビルドを提供していないので、レビューしてもらう時にステージングサイトやスクリーンショットを主導で用意しなければならない。

解決手法

  • PR作成時に docs ディレクトリに変更が入っていれば、Read The Docs上でのビルドとホスティングを有効化し、アクセス用URLをPRに書き込む。

インストール方法

  1. Read The DocsプロジェクトをGitHubリポジトリと接続しておく、あるいはGitHub Webhookと統合しておく
  2. Read The Docsプロジェクトに rtd-bot ユーザをメンテナーとして招待しておく。
  3. Gitリポジトリ.github/config.ymlrtd.project を作成し、Read The Docsプロジェクト名とその言語を指定する
  4. GitHub上でrtd-botを有効化する

注意点

  • いまのところ、Read The Docs側の設定項目(例えばドキュメントの場所など)には柔軟に追従できません。
  • 現バージョンのRead The Docsにおける「メンテナーの招待」は、「全権限の委譲」にほかなりません。不安な場合はご自身でProbotをホストいただけます。

技術的な工夫と、今後の開発の予定

このプロジェクトではCommitizenSemantic Releaseを導入しました。思っていたよりも使いやすく、またDependabotがすでにcommitizen風コミットコメントに対応していたため、特に導入に障害はありませんでした。MavenやGradleでも使えたら良いんですが、そこはまだハックが必要そうです。

Read The DocsのAPIはまだ機能不足が目立ちます。最近出たAPI v2でも認証機能すら無く、公開データへの読み取りアクセスしかできません。 今回はこの問題の回避にPuppeteerを使っています。つまり管理画面をNode.jsで操作しています。だいぶダーティハックですが、利用規約上も問題ないはずです。画面の更新には逐次追随する必要がありますが、過去の経験から言ってRead The Docsの画面はさほど大きく変更が入らないと思われます。

直近の予定としては、i18nには対応したいと考えています。他にも対応必要な設定があればROADMAP.mdに追記のPRを送ってください。