Kengo's blog

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

JavaウェブアプリプロジェクトにJavaScript/TypeScriptなどの静的アセットをどう配置するか

以前のJavaウェブアプリ開発では、JavaScriptをはじめとした静的アセットはsrc/main/webappディレクトリに配置するのが普通だった。そこに置くことでmaven-war-pluginのようなビルドシステムが.warファイルの中に突っ込んでくれる。この挙動は今でも変わらないが、src/main/webappディレクトリに静的アセットを直接置くにはいくつかの問題がある:

  1. TypeScriptコンパイラやBabelのような、静的アセットに事前処理を施す手法が普通になった。
    • src/main/webappに処理後のリソースを置くようにもできるが、mvn cleanなどで処理後のリソースが削除されるようにする手間を考えるとtargetディレクトリ直下にあるwebappDirectoryに置くのが無難と思われる。
    • よってsrc/main/webappには事前処理を必要としないアセットのみを置くことになるが、次に挙げる理由からそれも必要なくなってきている。
  2. フロントエンド開発が複雑化し、サーバサイドと同一プロジェクト・モジュールで開発することが難しくなった。
    • ここで言うフロントエンドはサーバからのレスポンスを受け取ってユーザ向けにインタフェースを提供する部分。HTML5アプリだったりモバイルアプリだったりする。
    • フロントエンド開発で使うビルドツールやライブラリ、development workflowは必ずしもJavaのそれと一致しない。例えばブラウザやOSのプレビュー版が出たときにフロントエンドだけテストを回すといったことができればより高速に検証できる。またJavaScriptライブラリはJavaライブラリ以上にこまめなバージョンアップが必要となるケースが多い(脆弱性対応とか、OSアップデート追随とか)ため、フロントエンドを独立に更新していけるプロジェクト体制を整えることが必要になる。
    • frontend-maven-pluginを使ってJavaのビルドツールに統合することは可能だが、そもそも開発者が異なるケースではあまり意味がない。OpenAPIやgRPC、GraphQLなどのインタフェース定義をフロントエンドとサーバサイドの間に入れて独立に開発していく体制を組むほうが良い。
  3. 静的アセットはTomcatのようなサーブレットコンテナではなく、Amazon S3のようなストレージサービスやCDNによって配信することが好ましい場合が多い。
    • 例えばGoogle Cloudのドキュメントでは、アプリによる静的アセットの配信をstraightforwardだが欠点のある手法として紹介している。

この問題を解決するために、大きく分けて3つの手法を紹介する。

1. 同一プロジェクト内で2つのビルドツールを併用する

サーバサイド開発にはMaven/Gradleを、フロントエンド開発にはnpm/Yarnを利用するが、プロジェクトは分割しない手法。

この手法1.を採る場合、アセットは .war.jar に同梱する形が最もやりやすい。例えばSpringだと同梱されているリソースをStatic Resourcesとして配信可能。懸念されるサーブレットコンテナの負荷低減は、HTTPレスポンスのキャッシュやCDNによって実現することになる。

1.1. サーバサイドをMaven/Gradleで、フロントエンドをMaven/Gradleで包んだnpm/Yarnで開発するケース

サーバサイドとフロントエンドを同一のビルドライフサイクルに乗せるため、frontend-maven-plugin/frontend-gradle-pluginを使って、Maven/Gradleからnpm/Yarnを実行することになる。フロントエンド開発の複雑さがあまりない場合、Java開発者がフロントエンド開発も兼ねる場合に重宝する。例えばsrc/main/typescriptにTypeScriptを、src/main/sassにSassを置くような体制。

プロジェクトルートにpom.xmlsettings.gradleを置いてフロントエンド用モジュールをMavenのサブモジュール/Gradleのサブプロジェクトとして扱うこともできるが、そこまでやるならば次に挙げる手法2で充分であろう。

なおフロントエンドの成果物をMaven Repositoryにzipしてデプロイし、他プロジェクトから利用するということを以前試したことがあったのだが、zip/unzipを多用し性能が出ないのでおすすめしない。普通にファイルコピーで済ませるために、利用者(サーバサイド)と同一プロジェクト(Gitリポジトリ)に置くようにしたほうが良い。

1.2. サーバサイドをYarnで包んだMaven/Gradleで、フロントエンドをYarnで開発するケース

逆にYarnを主、Maven/Gradleを従とする手法。サーバサイドが小さいときは使えるかもしれないが、そこまでするなら手法3が素直。

2. サーバサイドとフロントエンドでプロジェクトを分ける

各モジュールの開発技法において、自由度を確保することを意識した手法。それぞれ異なる開発者が請け負う場合にはこういった形を取ることが多いのでは。Gitリポジトリを分けるかmonorepoにするかという点も考慮が必要だが、自分の開発経験(小型中心)だと分割の必要性を感じたケースは無いので、ここではmonorepoという前提をおいて考える。またフロントエンド=ウェブアプリとし、モバイルアプリ開発については触れない。

サーバサイドとフロントエンドのつなぎ目には、OpenAPIなどのAPI定義手法を利用する。サーバサイドとフロントエンドを突合する部分(CD、プロビジョニングなど)が複雑化する可能性はある(例えばspring-boot-vue-exampleではserverとclientのデプロイにスクリプトを使っている)が、多くの場合でさほど問題ないのでは。プロジェクト構成例は以下の通り:

interface/
  openapi.yml

server/
  src/
  pom.xml // 最終成果物は .war, executable .jar あるいはコンテナ

frontend/
  src/
  package.json // 最終成果物は .zip あるいはコンテナ

docker-compose.yml // あるいはTerraformとかCloudFormationとか?
README.md

この場合、フロントエンドの成果物はサーバサイドの成果物に含めない。またフロントエンド開発にサーバサイドの挙動をmockする必要があるが、Open APIならmock serverを利用できるし、その他のAPI定義手法でも似たものがあるはず。

3. サーバサイドをNode.js化するという選択肢

これはタイトルに全く沿わない手法であるが、真面目にありがちな話だと感じている。つまりJavaJavaScriptという異なる言語を同一プロジェクトで使用することが複雑さを生むならば、その原因を取り除けないだろうか?ということである。

そもそもNode.jsが発展しTypeScriptのような型を使った開発も可能な現代において、サーバサイドとフロントエンドで異なる言語を使うモチベーションがどの程度あるだろうか?

サーバサイドをNode.jsにすれば、プロジェクトの複雑さを除くことができる。性能に直結する非同期処理もCompletableFutureRxJava、spring-webfluxで使われるReactorに比べればJavaScriptasync/awaitの方が簡潔になるし、何よりJavaScriptは言語自身が「メインスレッドをブロックしない」ことを前提に設計されているので、うっかりblocking処理をサーバ内に書くことを避けやすい。 もちろんJavaの方がやりやすいこともたくさんある。またJVMの進展は非常に目覚ましいものがある。運用の知見など積み直しになるものもあるため即置き換えとは行かないし、する必要もない。結局道具は適材適所なので、Javaエンジニアだと自分を狭く定義するのではなくて、JavaもNode.jsもTypeScriptも学んで使いこなせばいいし、KotlinやDartのような新しい言語とそのコミュニティにも目を向けていければ良いのだと思う。

まとめ

小型のプロジェクトでは手法1.1、専任のフロントエンド開発者がいる場合は手法2を使うことをまず検討すると良いだろう。

Javaプロジェクトにおけるリリース周りの手法あれこれ

考慮する点

成果物のデプロイ

ビルドの成果物(artifct)をアップロードすること。アップロードと公開は分けて考えることに注意。デプロイ先にはいくつか候補がある:

  1. GitHub Packages (旧GitHub Package Registry)
  2. Maven Central Repository
  3. Docker HubなどのDocker Registry

GitHub Packagesはコンテナも.jarもまとめて置けるが、コミュニティ標準の場所ではないので利用する際にひと手間必要になる。プライベートプロジェクトの場合は積極利用することになりそう。FOSSなら基本的にMaven Centralに置くことになる*1。プロジェクトによっては.jarファイルとしてではなくコンテナとしてデプロイすることもあるだろう。

リリースノートの作成

CHANGELOG.mdsrc/site以下のファイルを手動でメンテナンスする手法だけではなく、コミットコメントやPRからリリースノートを作成する手法もある。

  1. 手動管理
  2. CHANGELOG.md などファイルの自動生成
  3. GitHub Releasesによるリリースノート自動生成

特に開発者向けであれば、CHANGELOG.mdではなくGitHub Releasesを使うことで、変更内容の伝達を自動化できる。例えばDependabotは依存先の更新を提案する際にリリースノートの内容をPRに含めてくれる。ファイルの頒布が必要な場合でも、GitHub Releasesも合わせて使ったほうが良い。

バージョンの管理

利用者の利便性のため、Semantic Versioning 2.0を使うことが前提になる。ただ混乱を避ける意味でもバージョン 0.x.x の利用は避けたほうが良いかもしれない

  1. 手動管理
  2. 自動的に決定する

バージョンを決める作業自体は自動化が進んでいて、Conventional Commitsから生成するものがsemantic-releaseだけでなく様々あるので使ったほうが良い。私はプラグイン機構がしっかりしているのでsemantic-releaseを使っている。

プロジェクトのバージョンをpom.xmlbuild.gradle, gradle.propertiesといったファイルで管理するのが普通だが、最近はGitのTagを見るものもある。Tagを見ることで「ファイルに書いてあるバージョンをインクリメントする」ためのコミットが不要になるのが利点。例えばDraft Releaseを本Releaseに昇格させる(ブランチの最新コミットにタグを打つ)ことで公開処理を回す(tagイベントに紐付いたワークフローを回す)処理がシンプルになる。のだが、SNAPSHOT運用と相性が悪いのと、ファイルに書かれたversionの信頼性がなくなるのとで、あまりJavaプロジェクト向きではないと考えている。

各手法について

Maven (maven-release-plugin)

mvn release:prepare && mvn release:performを実行するだけで必要な処理が完結する。Maven公式の機能で完結し、歴史も長いのでノウハウも蓄積されている。

シンプルだが、Tag作成をリリースの起点にできない(Tag打ちがプラグインによって行われる)ために自動化には向かない。多くの場合、リリース用の(Jenkins)Jobを作って手動で蹴るような運用になるのでは?pom.xmlの更新とGitへのpushがプラグインによって行われることもあり、自動化の際はプラグインによるコミットメッセージに[skip ci]を含めたり設定を変更したりといった工夫が必要になる。

Maven (maven-deploy-plugin)

Mavenでリリースを自動化する場合はmaven-deploy-pluginによるデプロイ手法を採ることになる。特にmaven-versions-pluginのsetゴールと組み合わせることが多いはず。

リリースに使用するバージョンをTag名などを経由してnewVersionプロパティに渡してversions:setを実行、deploy:deployでデプロイした後に再度versions:setでSNAPSHOTバージョンに変えてGitにpushする運用。release-drafterが使えるのでリリースノート管理もしやすい。

この場合でも、Tagが打たれたrevisionのpom.xmlには実際にリリースされたversionとは異なる値が書いてあるはず。versionの信頼性という点で問題が残る。

Maven (maven-semantic-release)

version mismatchが気になる場合は、semantic-releaseの利用を検討する。これはmasterブランチにpushされた変更をすべてリリースするという手法である。

バージョンの決定はsemantic-release本体が、リリースノートの作成は@semantic-release/github@semantic-release/release-notes-generator@semantic-release/changelogが、GitのcommitやTag打ちやpushは@semantic-release/gitが行うことになる。すべてのプロセスが自動化されるので、運用の負担は最も低くなる。またversionの信頼性も確保できる。

Conventional Commits(厳密にはAngularのルール)の利用を全開発者に強制しなければならないこと、Javaプロジェクトなのにpackage.jsonを使わなければならないこと、masterブランチを常にリリース可能にすることが許容できるならばこの手法が最善と思われる。

ライブラリはともかくアプリやサービスだとPR毎リリースが受け入れられないことはあると思うが、こちらのFAQに目を通した上でも必要と判断されるなら、リリースを意図的なタイミングでトリガする手法を取り入れることもできる。

Gradle (gradle-semantic-release-plugin)

semantic-releaseはGradleでも使える。詳細は以前の投稿を参照。

Jib

GitHub repoにBuild container images for your Java applicationsと書いてあるように、Javaアプリをコンテナ化するときに使えるツール。Maven/Gradle両対応。デフォルトでもコンテナのレイヤをうまく分けたり、warプロジェクトならJettyを同梱したりしてくれる。

依存を必要としないのはありがたいが、dockerひとつ追加するだけならあまり苦ではない(GitHub Actionsだとデフォルトで付いてくる)のでそこまでの利点という感じではない。ドキュメントにちょいちょいKubernetesとの組み合わせ方について言及されているので、Kubernetes使いにとってはやりやすいところがあるのかも。

Docker

MavenやGradleを使ってデプロイするのではなく、CIサービスでdockerコマンドを実行する。Maven/Gradleはpackage/assembleするまでが責務で、デプロイはdocker pushで行う運用になる。前述の通りdockerコマンドの導入は多くのCIサービスでfirst classのサポートを受けているので、セットアップは非常に楽。最近はmulti-stage buildもあるので最終成果物も充分に小さくしやすい。

一見やりやすいが、開発作業中にdockerコマンドが実行されない可能性をはらんでいる。これは開発者が触るものがリリースされるものと異なるものになるということで、微妙なリスクになる。開発作業中もmvnw/gradlewではなくdockerコマンドを使うようにする(付随する課題も解決する)か、コンテナにデプロイされたアプリを使うE2Eテストを早期に統合すると良いかもしれない。

なおイメージを小さくするのに jlink を試してみたいのだが、現状ではalpineで動くOpenJDK(Portolaプロジェクト)が公開されてない?ようで、2018年5月時点で有効だった手法が使えなくなっている。のでalpineイメージをベースにするためにはDockerfileがけっこう複雑かつ管理の難しい状態になってしまうのが難点。この辺の情報はおそらくこの2018年11月時点のブログ記事が最新。

docker-compose

コンテナの利用をもう一歩進めて、データストアやHTTPサーバもコンテナ化してアプリとの依存関係やそのバージョンをdocker-compose.ymlで管理する。 ここまで行くとより良いデプロイ手段というよりは、より良いプロビジョニングやポータブルな実行環境の方を実現したい段階だと思われる。

注意することはDockerを単体で利用する場合と同様だが、アプリのコンテナが依存するコンテナ(データストアなど)をアプリをビルドするたびにビルドするような運用にならないように注意する必要がある。具体的には、ADDするファイルを最小限にすることで、スキーマ定義やマイグレーションスクリプトが変更されたときなど、必要なときだけ依存するコンテナがビルドされるようにする。

*1:Maven Centralも十分に早いしHTTPSもサポートされたので、個人的にはJCenter使うモチベーションがない

draft: より良いチームを作る2019

ということで形にするために書き下しています。

背景にある考え方

  • 高い利益を上げる製品、キャリアを高める成長機会、より良い待遇と給与、良い顧客。こうした良いモノすべての源泉が「良いチーム」である。
  • しかし変化の激しい時代、ひとつの固定した「理想のチーム像」を作ることは不可能である。よって現代のチームは「時や状況に応じて学び変化できるチーム」を目指すべきである。
  • この文書では「最高のチーム」「理想的なマネジメント」ではなく「より良いチーム」を作ることを目標とする。

良いチームの指標

  • 頻繁に質の高い学習ができる。

    • 頻繁に:組織の目的によるが、少なくとも月イチ程度にretrospectiveを行う。開発組織の場合はweekly iterationやbiweekly iterationを目指す。突発性インシデントへの対応が規定され共有されているため、ダメージを抑えpostmortemを行い学習し製品にfeedbackできる。
    • 質の高い:すべての施策に共有された目標がある。このfactがほしいと思ったときに探し出せる、あるいは新規に測定できる。problem solvingについてチームが理解している。他社事例や公開された研究結果などを適宜参照する。自分たちの学習したことも可能な限り公開し、ひろくフィードバックを受け付ける。
    • 学習ができる:人のせいにしない、失敗を成長の糧として捉える、blameless postmortemの文化がある。個人の意識や努力でカバーするのではなく、組織や自動化でカバーする。目指す像が全員に共有されている。目標に対するオープンな議論とdisagree and commitのための責任の所在(後述)が明確である。
  • 個人の個性と意思が尊重され、結果としてモチベーション高くモノづくりができる。

    • 多様性を重視する、オープンな議論を行う、役職を発言と紐付けない。認知的不協和を抑えるためにも、個人が謙遜さとオープンさを身につける必要が、組織が心理的安全性を満たす必要がある。
    • マネジメントが「上司」ではなく「支援者」であることを信じてもらう。servant leadershipを実行する。
    • 各個人が学びたいこと、挑戦したいことの把握に努める。得た情報を現状に照らし、勤務の一環として成長機会を作る努力をする。学びたくないこと、目を背けたいこととの折り合いを共に考える。
    • 健康状態や家庭の事情に気を配る。可能な限り勤務時間や貢献手法に制限を設けない。場所や時間を縛る働き方を排除する。
  • 各自の役割が明確になっている、ボトムアップトップダウンのバランスが良い。

    • ブレークスルーの源泉は常にチームにある。チームのリソースをどこに割くかを検討し優先度をつけるのがマネジメントの役割。
    • ボトムアップの力を信じてempowermentすることと、組織目標やスコープを明確にすることは両立する。
    • マネジメントはまずチームのMissionとVisionを明確にし、チームのベクトルを合わせる必要がある。
    • チームを信じられない、チームに任せられない理由があるならばまず第一に排除する。言語の壁、文化の壁、情報の不対称性、習慣の違いなどが該当する。

従来の「責任」と、これからの「責任」

我々は(失敗したときに)責任を取れという言い方をすることがあるが、それになんの意味があるのだろうか?

失敗によるダメージを抑えるためにマネジメントはリスク管理をするべきで、管理が足りていなかったなら反省し学ぶ必要がある。 また管理できなかったダメージは既に存在するので、再発防止だけでなくケアの方法も考える必要がある。

責任の本質は、情報と権限を明確にして状況に応じて速やかに学び行動できる組織づくりではないか。 責任を取らせることが必要なのではなく、責任を預けるに足る能力(組織づくりやリスク管理)を持つ人材を責任者に据えることと、能力が足りないと判断したときに上長の判断で外せることが必要なのではないか。

問題点を発見する運用手法

One on oneとOKR。MBO-Sでも良いけどOKRではダメな理由がないのと、OKRの方が明確に失敗を織り込んでいる(Key Resultsがすべて達成できてるのは逆に良くないという前提が共有されている)ので良いと思う。あとフォームを利用したマネジメントへの匿名FBも有用。あとで掘り下げる。

コーチングが問題発見の手法になるかもしれない。少なくとも聞き手と話し手の双方に問題発見を促す働きがある。

不確実性を早期に発見するために、目標は小さくし、Scrum手法を適用する。 ダメージを想定し備えるとともに、失敗したときのコンティンジェンシープランを持っておく。

良いチームを支える技術

手を動かすことと議論することにチームを集中させる必要がある。ここで言う議論はコンセンサスを作ることではなく知恵を出し合うことである。

手を動かす時間を作るために、作業は極力自動化する。バグの再現環境を作ったり、管理用の情報を揃えたり、やるべき作業をリマインドしたり、必要なライブラリやミドルウェアを揃えたり、コードレビューをしたり。 CIジョブでできるチェックは極力CIジョブで実施する。理想的には1日あたり4時間以上集中して作業できる状態にする。時間はまとまって確保できるようにし、フローに入れるようにする。

議論はあらかじめ目的を明確にして、結論の出し方を決めておく。ファシリテーターを置く。ここでは思考や知見を積み上げることを目的としているため、アウトプットは文書化し検索可能にする必要がある。

ほか

  • TeachingとCoachingの使い分け
  • 組織パターン
  • 売上、利益、バジェット

参考書

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で統一されているので、長いものには巻かれておく