Kengo's blog

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

WIP: 2020年やりたいこと50

2020年やりたいこと100 - ややめもを見て良い取り組みだと思ったので書いてみたのですが、100も出てこないのでとりあえず半分でお茶を濁す作戦。

健康(1~10)

  1. 週2回の筋トレを継続
  2. 平日は10,000歩以上歩く
  3. 体重を72kg±2程度に抑える
  4. 24時までに寝る
  5. 平日のコーヒーをやめる
  6. 水やお茶を1日2リットル以上継続して採る
  7. 朝ごはんの菓子パン依存率を減らす、食パン+野菜や肉などで代替する
  8. 皮膚のプロアクティブ治療を継続する
  9. 暗いところでスマホで読書する習慣をなくす、視力低下を防ぐ
  10. 平日の昼寝を継続する

育児(11~20)

  1. 息子氏がひらがなを読めるようにする
  2. 息子氏を新幹線に乗せる
  3. 息子氏の小学校を決める
  4. 息子氏の中学校以降を検討する
  5. 絵本を継続して読みきかせる
  6. 毎朝30秒のハグを息子氏が嫌がらない限り継続する
  7. 幼稚園への登園中に日本語で息子氏にどんどん話しかける
  8. 息子氏を動物園や水族館、スポーツの試合に連れて行く
  9. 息子氏の友人一家と遊ぶ
  10. 日本語を話す息子氏の友人を作る

家庭(21~30)

  1. 家族と中国国内旅行に行く
  2. 家族と日本国内旅行に行く
  3. 音声認識コントローラを導入する
  4. 無駄を省き効率化を進めることで、平日夜の時間を創出する
  5. 平日朝の忙しさを緩和する(≒息子氏をきちんと起こす、息子氏の朝食や着替えをきちんと済ませさせる)
  6. 投資や副業などで収入を増やす
  7. 映画や美術館、新しいショッピングモールなどに行く習慣を作る
  8. そのための日常的な情報収集を行う(WeChatなど)
  9. 実家との継続的な連絡を取る
  10. スケジュールや心持ちに余裕を持つ、息子氏の突発的な風邪などを常に想定に入れる

趣味(31~40)

  1. Skebでらきすけさんにライチュウを描いてもらう
  2. SpotBugs Gradle plugin v2をSpotBugs Organization下でリリース
  3. SpotBugs 4.0.0安定版をリリース
  4. Sphinxに3つ貢献する(docker imageとか)
  5. n月刊ラムダノートに寄稿する
  6. JavaプロジェクトをひとつKotlinで書き直す
  7. VRゴーグルを導入する
  8. 週1冊の本を読む
  9. GitHub sponsorsに登録する
  10. ブログ記事を6つ書く

業務(41〜50)

  1. 部署の売上目標をつくり達成する
  2. 部署の利益目標をつくり達成する
  3. 部署の技術目標をつくり達成する
  4. 個人の技術目標を策定・達成する
  5. 職場のモニタを新調する
  6. 職場のポインティングデバイスを新調する
  7. 日本の技術イベント登壇
  8. 上海の技術イベント登壇
  9. 3回ほど社内技術勉強会で公開セッションをする
  10. M5StickCで職場用ガジェットを作る

2019年のFOSS活動状況まとめ

昨年のに引き続きFOSS活動状況をまとめます。2019年12月30日時点の情報です。

概要:昨年比22%増

GitHubのプロファイルページによると今年のpublic contributionsは1,166で、昨年が950だったので約22%増です。commit 61%のpull requests 21%なので、けっこう手を動かしてコードを書けたと思います。業務でのpublic contributionsはゼロなので、すべて趣味開発です。

f:id:eller:20191230233035p:plain
Contributions at GitHub in 2019

今年新規に作成したのは主に gradle-semantic-release-plugin, open-the-sesamespotbugs-gradle-plugin-v2 です。gradle-semantic-release-pluginについては3月に紹介記事も書いています。

7〜9月の Contributions が減っているのは勤務先における異動による影響です。これがなかったら1,500くらい行けたかもしれません。

SpotBugs周りの開発

2019年もSpotBugsが一番活発な開発でした。コア開発58 commitsと昨年比で減っていますが、SonarQube pluginGradle pluginは横ばいという感じです。

f:id:eller:20191230235144p:plain
Contributions for spotbugs/spotbugs in 2019

特に現Gradle PluginがGradle本体からのforkなため、internal APIに依存していたりAndroid開発へのケアが抜けていたりという課題が多かったので、半年ほどかけて準備をしスクラッチで書き直しています。Gradle Pluginポータルにはデプロイしたので、承認されしだい利用可能となるはずです。

今年の目玉はSpotBugsのダウンロード数がFindBugsのそれを上回ったことですね。あれから苦節3年弱、継続は力なりを体現した感じです。リリースもそうですがマニュアルサイトの作成と管理も結構関われているので、嬉しく思います。

その他

ひとつプロジェクトに関わらせていただいているのですが、まだ公開できる状態ではなさそうです。あまり積極的に貢献できていないとはいえ意義のあるプロジェクトですので、機が熟したらいろいろと紹介していければと思います。

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

追記 2019-12-12

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

speakerdeck.com

speakerdeck.com

*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の設定が必要です。

2020-01-12 追記

上記の書き方だとforked repoからPRを送ってもらった時に問題になるので、secret variableにアクセス可能な場合にのみ実行するような工夫が必要なようです。

    - name: Run SonarQube Scanner
      run: |
        if [ "$SONAR_LOGIN" != "" ]; then
          ./gradlew sonarqube -Dsonar.login=$SONAR_LOGIN
        fi
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        SONAR_LOGIN: ${{ secrets.SONAR_LOGIN }}

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環境にインストールする方法があるので、コンテナを使う必要性もあまりありません。