Kengo's blog

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

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するファイルを最小限にすることで、スキーマ定義やマイグレーションスクリプトが変更されたときなど、必要なときだけ依存するコンテナがビルドされるようにする。

追記 2019-12-12

buildpacks.ioでHerokuで使えるbuildpackを標準化しようとしているらしい。

speakerdeck.com

speakerdeck.com

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

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は標準となっているため、実際に学ばなければならないのはConventional 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の作成

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

SpotBugsプラグインを元にしたSonarQubeプラグインを実装する方法

SpotBugsプラグインの実装方法には公式ドキュメントがありますが、SpotBugsプラグインをSonarQube上で実行するための手法はまだ固まっていません。最近Guava Migration HelperのSonarQubeプラグインのリリースに動いているので、要点をまとめておきます。

SonarQubeプラグイン実装の基本

まずSonarQubeの公式ドキュメントに目を通しておきます。特にMarketplaceでの公開を前提としている場合は、Plugin Keyの命名に制約があったり、SonarCloudの利用が必須だったりしますので、制約についてよく読んでおくと良いでしょう。

Mavenプロジェクトを作る

Gradleサポートは公式ではないので、Mavenを選択します。SpotBugsプラグインと同じプロジェクトで管理すると、メタデータ作成(後述)で手間がかからないのでおすすめです。私のプロダクトではSpotBugsプラグインとSonarQubeプラグインとをサブモジュールとして管理するプロジェクトを使っています。注意点は以下の通りです:

  • SonarQubeプラグイン<packaging>sonar-plugin</packaging>にする。
  • sonar-packaging-maven-pluginの設定に<basePlugin>findbugs</basePlugin><requirePlugins>findbugs:3.5</requirePlugins>を入れる。sonar-findbugs v3.5は初めてSpotBugsを導入したバージョンなのでこれ以降のバージョンを指定するのが良い。
  • SpotBugsプラグインが何らかのライブラリに依存している場合、普通のパッケージとshadedパッケージと両方を用意すると良い。 shadedはAntユーザやCLIユーザなど推移的依存が自動的に解決されない場合(プラグイン手配置が必要な場合)に便利。 shadedだけ用意すると、推移的依存が自動的に解決される場合に同じライブラリを複数回ダウンロードするリスクや、dependencyReducedPomによって依存先のメタデータが隠匿されてしまう(例えばライセンス管理がやりにくくなる)問題も生じる。

rule.xmlを生成する

SonarQubeはfindbugs.xmlやmessages.xmlを読まないので、SonarQubeが読める形式でルールに関するデータを提供する必要があります。私はこれをMavenプラグインで自動生成しています。ルールによってタグを変える必要がある大型プロジェクトじゃない限り、これで十分だと思います。

コードを書く

基本的にはPlugin.javaとRulesDefinision.javaの2つで足ります。以下がサンプル:

注意点は以下の通りです:

  • Context#createRepository()の第一引数は"findbugs"である必要がある。 それ以外の値だと、sonar-findbugsが検知してくれない その結果 findbugs-include.xml にBugが登録されないので、SpotBugsプラグインが解析に使われても結果一覧に出てこなくなる。

リリースする

あとはいつもどおりMaven Centralに公開した上で、SonarSourceコミュニティのフォーラムで申請すれば通るようです。私のプラグインはまだ申請中です:

Maven3電子書籍の利用され具合(2017年)

ほそぼそと続けているMaven3の入門書「Maven3のはじめかた」ですが、最近はprh入れたりリポジトリの適当すぎた説明を修正したりといった微修正を行っています。

www.gitbook.com

で、意外にも年間2万人のユニークユーザがついたようです。以下が2016年12月〜2017年12月のデータです。

f:id:eller:20171230222121p:plain

どこからそんなユーザが流入したのか気になるところですが、KDDI ChatWork、Teratail、Yahoo!の社内サイトと思しきものがリファラにありました。どうやらMaven用語の説明に活用されたようです。今は正直プラグイン実装手法の説明が厚く用語解説が貧弱なので、用語解説系をもうちょっと強化したほうが良さそう。

検索サイトはGoogle, Yahoo, Bing, 楽天の順。インドやベトナム、ロシアの検索サイトと思われるものもありました。現地で働いている日本人が使っているのでしょうか。今日Google Analyticsを入れたので、次回は検索キーワードあたりも調べられるでしょう。

国別に見ると日本、米国、シンガポール、オランダ、中国の順です。米国とシンガポールには日本人技術者多い印象でしたが、オランダが入ってくるとは意外でした。ただユニークユーザ数は低いのでヘビーユーザがいらっしゃるのかもしれません。

f:id:eller:20180101222920p:plain

この本は今後も継続してメンテしていきます。コードは全てGitHubで公開されていますので、内容へのフィードバックやPRもお待ちしています。

Mavenプロジェクト用Travis CI設定(2017年末)

今年はビルド周りで自分の常識がいろいろ変わった年だったので、既存の知見もあわせてまとめます。

Mavenのバージョンを固定する

Travis CIは不定期にビルド用仮想マシンを更新しますが、そのタイミングでの最新のMaven仮想マシンに入れるようです。このブログ投稿時だと3.5.2が入っています。

もっと新しいバージョンを使う、あるいはMavenのバージョンを固定するには、Maven Wrapperを使うと良いでしょう。Travis CIはプロジェクトルートディレクトリにmvnwスクリプトがある場合はそれを優先的に使います。

mvnコマンド指定時の注意点

scriptinstalldeploy などのフェーズで mvn コマンドを明示的に実行する場合、 -B オプションを忘れないようにします。このオプションによってMavenインタラクティブなログを抑制するようになり、ログサイズが激減します。Travis CIはログサイズに上限があるため、うっかり忘れるとビルドが不安定になります。

グローバルにコマンドラインパラメータを指定する方法

グローバルにコマンドラインパラメータを指定する場合、それがローカルでの実行時にも必要となるパラメータならば、Maven 3.3から導入された .mvn/maven.config あるいは .mvn/jvm.config を使うことも可能です。ただし現状これらのファイルはコメントを書けないので、チームで使用する場合はどこかで説明が必要でしょう。

SonarCloudによるコード解析

FOSSならSonarCloudを使えば無料で解析できます。Travis CIには専用Addonがあって、MavenでSonarQube専用プロパティをややこしく設定しなくても、pom.xmlをよしなに解析してプロジェクトホームやIssueトラッカのURLを見つけてくれます。

なおコード解析をPRにも働かせる場合は、PRのもととなるブランチも同じリポジトリに存在する必要があります。これはトークンの復号化に必要なためです。

Travis CIによるSonatype Nexusへのデプロイ

これを取り入れるかどうかは意見が別れるところだと思います。というのもこれをやるには、暗号化済みとは言えGPG署名に必要なファイルやSonatype Nexusのパスワードをリポジトリにコミットする必要があるのです。

私の場合、個人プロダクトでは使わずに Maven Release Pluginを使うようにしています。ただしSpotBugsのような共同リポジトリの場合、リリース難度を下げ属人性をなくすために使用しています。

作業内容は単純に、GPG public keyring, GPG secret keyring そしてGPG passphaseとSonatype Nexusアカウント情報を含んだ settings.xmlを暗号化してリポジトリにコミットし、それをデプロイ時に使うだけです。以下のPRが参考になると思います。

GPGのkeyringは以下のようにコマンドで出力できます。

$ gpg --export $KEY_ID > .travis/pubring.gpg      
$ gpg --export-secret-key $KEY_ID > .travis/secring.gpg      

またリリースの属人性をなくすため、リリース手順書も合わせてコミットしておくと良いでしょう。settings.xmlのテンプレートもあると、引継や暗号化済みファイルの更新に役立ちます。

Contributorに対するリマインド

GitHubPRやIssueにテンプレートを作成することができます。またContributing Guidelineを作成することもできます。従来はこれがユーザに対するほぼ唯一のIssue/PR作成時のリマインダでした。

ただチェックボックスをテンプレートに入れたとしても、すべてのContributorがこれを使ってくれるわけではありません。ので、個人的には以下のブログで紹介されているProbotに期待しています。