Kengo's blog

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

Gradleのjvm-test-suiteプラグインがテスト周りの定型コードを排除するのに便利そう

Gradle v7.5の時点ではまだIncubating段階の機能ではあるのですが、Gradleの新しいプラグイン jvm-test-suite がいい感じなので紹介します。

docs.gradle.org

解きたい課題:サブモジュールや統合テストが出てくるととたんに面倒になるビルドスクリプト

Gradleは設定をDSLで記述するので基本的には何でもありなのですが、やはり定形コード(boilerplate)は少ないほうがビルドスクリプトの見通しも良くなります。もちろんGradleは「設定より規約(Convention over Configuration)」の考えを持っているため、ある程度は空気を読んでSourceSetやTaskを自動的に生成してくれます。しかしテスト周りにおいてはこうした自動生成は十分ではなく、次に挙げるような課題がありました:

  1. サブプロジェクト全てに対して実行したタスクのレポートを統合するのが面倒。ここでレポートとは単体テスト実行結果、ないしJaCoCoによるカバレッジ測定結果などを指す。
  2. スモークテストや統合テストといった単体テスト以外のテストを実行する際に、SourceSetやConfiguration(依存管理)、Taskなどを自分で書かなくてはならない

例えばJaCoCoのカバレッジ測定結果をSonarQubeに渡すために、SpotBugsプロジェクトでは以下のようなタスクを自分で書いて実行しています。一部はプロジェクト固有の課題を解決するためのコードですが、複雑さが伝わればOKです:

task jacocoRootReport(type: JacocoReport) {
  description = 'Merge all coverage reports before submit to SonarQube'
  def reportTasks = project.getTasksByName("jacocoTestReport", true).minus(rootProject.jacocoTestReport)
  dependsOn reportTasks

  executionData.setFrom reportTasks.executionData
  sourceDirectories.setFrom reportTasks.sourceDirectories

  // Only enable class directories related to non-test project
  classDirectories.setFrom files(reportTasks.classDirectories).filter {
    !it.toString().contains("-test") && !it.toString().contains("Test") && !it.toString().contains("junit")
  }

  reports {
    // JaCoCo SonarQube plugin needs a XML file to parse
    // https://docs.sonarqube.org/display/PLUG/JaCoCo+Plugin
    xml.required = true
  }
}

jvm-test-suiteプラグインの新規性

jvm-test-suite プラグインは java-library など既存のプラグインを導入しているプロジェクトであれば既に有効化されています。既存のプラグイン達とのシームレスな統合を前提に開発されていると言っていいでしょう。

また別種のテスト追加やレポートの統合機能もこれに統合されており、例えば前述のJaCoCoレポート統合は test-report-aggregation プラグインによって提供されますが、 jvm-test-suite プラグインの提供するレールに乗っていれば testCodeCoverageReport というTaskが自動的に生成されます。先述の定形コードがまるっと削除できるわけです。

後述するSourceSetやConfiguraitonの自動生成と合わせて、多くの定形コードを排除するのに役立ちそうです。

jvm-test-suiteプラグインの提供するレール

jvm-test-suiteプラグインを適用する上で注意すべきはひとつ、テストの実行に必要な情報を testing Extensionに集約することです。

詳しい説明は公式サイトに譲りますが、例として spotbugs-gradle-plugin で試している設定は以下のようなものになりました:

testing {
    suites {
        val test by getting(JvmTestSuite::class) {
            useJUnitJupiter()
            dependencies {
                implementation(gradleTestKit()) // Gradle v7.6+ が必要。 https://github.com/gradle/gradle/issues/19849
            }
            targets {
                all {
                    testTask.configure {
                        maxParallelForks = Runtime.getRuntime().availableProcessors()
                    }
                }
            }
        }
        val functionalTest by registering(JvmTestSuite::class) {
            useSpock()
            testType.set(TestSuiteType.FUNCTIONAL_TEST)
            targets {
                all {
                    testTask.configure {
                        description = "Runs the functional tests."
                    }
                }
            }
        }
    }
}

これだけで functionalTest Taskと、その実行に必要なSourceSetとConfiguraitonが自動的に生成されます。useJUnitJupiter(), useJUnit(), useSpock(), useTestNG()など依存やバージョンを一括管理してくれる便利メソッドもある(Gradle 7.6からuseKotlinTest() も増えそう)ため、今後は testImplemenentation などのConfiguraitonを自分で設定する機会も激減しそうです。

まとめ

jvm-test-suite プラグインを利用することで、ちょっと複雑なGradleプロジェクトでは頻出だったテスト周りの定形コードの多くを省くことができます。 サンプルプロジェクトがGradle公式から提供されているので、興味のある方は確認してみてください!

github.com

GitHub Actions 最近のやらかし一覧(2022年夏)

2020年のやらかし一覧 に続いて、最近のやらかしも残しておきます。

PRがマージされたときだけPR番号を取得しそこねる

PR番号を取得するのに GITHUB_REF を使いがちですが、マージされたときだけはマージ先ブランチ名が入ってきてしまうので完全ではありません。

# bad
on:
  pull_request:
    types: [ opened, synchronize, reopened, closed ] # closedイベントも拾いたい
  jobs:
    bad-case:
      runs-on: ubuntu-latest
      steps:
        - run: |
            PR_NUMBER=$(echo $GITHUB_REF | sed -e 's/[^0-9]//g')
            echo "PR番号は${PR_NUMBER}です" # merge時には空文字が入ってしまう

pull_request イベントにちゃんと number が入っているので、これを使用すればOKです。

# good
on:
  pull_request:
    types: [ opened, synchronize, reopened, closed ]
  jobs:
    good-case:
      runs-on: ubuntu-latest
      steps:
        - run: |
            echo "PR番号は${PR_NUMBER}です"
          env:
            PR_NUMBER: ${{ github.event.pull_request.number }}

自分が配布するActionでsemantic tagsを提供しないほうが良いと思っていた

GitHub公式のセキュリティガイドに、クリエイターが信用できるときだけタグに依存して良い=通常はフル長コミットSHAを使って依存せよ、と書いてあります。ので自分を信用するやつなんておらんやろの精神で v1v1.2 のようなsemantic-tagは提供してきませんでした。

ところが同じ公式ドキュメントで、semantic tagsを提供してねと推奨しているんですね。

Add a workflow that triggers when a release is published or edited. Configure the workflow to ensure semantic tags are in place. You can use an action like JasonEtco/build-and-tag-action to compile and bundle the JavaScript and metadata file and force push semantic major, minor, and patch tags. For an example, see this workflow. For more information about semantic tags, see "About semantic versioning."

有名企業でもやらかす世界線で個人開発者を信用するのはやめたほうが良いとは今でも思っていますが、信用するしないはユーザが決めるものなので、Action提供者としては選択肢を残してあげるほうが良さそうです。

13年ぶりにストレングスファインダーをやった

ストレングスファインダー、今はクリフトンストレングス(CliftonStrengths)と呼んでいるそうですが、13年前に新卒入社したときも本を買ってテストを受けたことがありました。

当時は慎重さ・戦略性・規律性・内省・収集心が強みだという結果が出ていました。「あらゆる道のりには、危険や困難が待ち受けていると考えている。日課や秩序正しい計画に従うことを好み、決定や選択を行う時に細心の注意を払う。あらゆる種類の情報を蓄積したり自分の頭の中で考えるのが好きで、知的な討論が好き。」ということで雑に言うと石橋叩いて計画するタイプだったんですね。

さて新しく入社した会社がクリフトンストレングスをまた受けさせてくれました。今回強みとして出た資質は「学習欲・最上志向・収集心・アレンジ・原点思考」でした。内省は7位、慎重さは15位、戦略性は16位、規律性はなんと27位に落ちています。この変化について考えてみたら人生経験がわりとダイレクトに反映されてるかなと思ったので書いてみます。

資質変化の裏側にある人生経験

計画から経験主義へ

「秩序正しい計画に従う」規律性と「決定や選択を行う時に細心の注意を払」う慎重さが落ちて「結果よりも学習すること自体に意義を見出」す学習欲と「一度作り上げた構成にこだわらず、作り変えることをいとわない柔軟性を備える」アレンジが浮上したのは、業務での意思決定において経験主義を採用することが増えたことと関係がありそうです。つまりスクラムの採用と不確実性への理解です。

少なくとも学生時代には、私は「世の中の問いには答えがある」と考えていた節があります。理想のプログラム、理想の情報システム、理想のマネジメント、理想の自分があり、それを探し出してその具体化についてのみ考慮すればよいという考えです。これが正ならば、細心の注意を払って作った秩序正しい計画に従って行動することが最も効率的に理想を実現する方法のはずでした。

実際には答えのない問い、あるいは答えが変わりゆく問いもあります。理想も現状も移ろうので、戦略も継続的に更新しなくてはなりません。そのためには失敗というプロセスから学ぶ準備と姿勢、状況の変化に対応した新しい戦略に作り替えるための柔軟性が必要になります。これを身につけられた13年だったのかもしれません。

歴史・書籍・研究から学ぶ

「過去を調べることにより、現在を理解」する原点思考と「一度作り上げた構成にこだわらない」アレンジが浮上したのは、もともと高かった「あらゆる種類の情報を蓄積」収集心が歴史・書籍・研究から学ぶスタイルとうまくかみ合ったからだと考えています。なお「物事の理由と原因を追求」する分析思考が6位に入ってきたのも関係あるかなと思っています。

歴史・書籍・研究から学ぶというのは、今目の前にある課題や疑問に対して自分の頭で考えるだけではなく、自分の外に情報や発想を求めるということ、自分の向き合っている問題領域について先人が何を考え行動してきたのかを知ることです。

例えばビジネスで出会う課題のいくつかについては、書籍にすでに情報がまとまっていたりします:

また組織がどのように失敗してきたか、だけでも以下のように様々な書籍が出ています:

自分の頭で考えることも重要なのですが、考える材料が揃わないうちに直感ベースで突き進んでしまうと既知の落とし穴に容易にハマるのが人間です。例えばITエンジニアの業務の範疇では、FlickrのDevOpsGoogleのError budgetを知っているのと知らないのとではシステム運用に関する発想が大きく変わるはずと思っています。

単語を知り概念に名をつけるだけでも、考察の幅が大きく広がります。意義や新規性のある意見を持つためにも、まず先例や類似事例について学ぶことは重要ですし。人間や組織がどう考え行動するのかを知ることで人生の意外なところで役立てられるのかなとも思います。

組織とは人だ、ではマネジメントには何ができるのか

内省よりの話が続いたので対外的なところ、マネジメントについて着目してみます。チームとの関係性の築き方に影響しそうなのは、2位の「個人やグループの改善を促す方法として長所に着目」する最上志向と、8位の「各人のユニークな資質に関心を持ちます。異なるタイプの人たちの集団をまとめ、生産性の高いチームを作ることに長け」る個別化です。

実際に生産性高いチームを作れていたかは他の方に評価を譲るとして、尖った個性的な人材をまとめて開発チームを作ること自体は好きでした。残念ながら私は自分大好き人間なので、傾聴すべき場面で自分語りをしたり自分流を押し付けてしまうなどの問題行動もあったのですが。他者の強みを知ること、強みを伸ばすこと、強みが摩擦で損なわれないようにすること、チームの凹みをカバーすることには時間を割き関心を払ってきたと思います。

異なるタイプの人たちの集団をまとめるのに重要なのは、マネジメントの期待を明確にすることだと考えています。最低限の要求を明文化し示すことで、それだけ守れば自由にやっていいのだというあそびが生まれるためです。期待とはアウトプットかもしれませんし、企業文化かもしれませんし、レゾンデートル(存在理由)かもしれません。MVV(Mission, Vision, Value)だとちょっと粗すぎるので、四半期か半年レベルの目標に落とし込む必要があると思います。

課題だと思っていること

経験主義が重要な考え方だとは言え、慎重さが必要なくなったわけではありません。目標を見直し続けるためにどういったデータを残すべきなのか、残したデータをどのように分析するか、失敗したときにどう戦略を切り替えるか、といった細かな内容を「とりあえずやってみて失敗する」前に準備しておかなくてはなりません。

また規律性も同様で、画一的な働き方が不要になった今でも残すべき規律はあります。ITエンジニアで言うならばアジャイルのセレモニーのような、働き方のリズムをつくる仕組みは従来どおり実行していく必要があると考えています。これによって透明性が確保されることで、経験主義が回り始めるからです。

これらはもともと気にできていた部分ですが最近あまりできていないのかなと思ったので、改めて見直していきたいと考えています。

まとめ

日々学習すべきなのは目の前の課題の”正解”ではなく課題解決の基礎体力であること、そのためには過去や外に目を向ける必要があること、マネジメントとして長所に目を注目して期待を伝えることの3点が大切だと学んだ13年でした。

一方で慎重さや規律性も重要なので、かつての強みを手放すことなく活用するべく見直しをかけていきます。

退職エントリ

14年勤めたソフトウェアベンダーを今月末で退職します。私が入社したころは新卒が3年で辞めるという話があって、漠然と自分も似たような感じになるのかもと思っていたので、まさかここまで長く在籍することになるとは想像していませんでした。お世話になった皆様、ありがとうございました。

職場近影(2018年1月)

一生に何度もあるイベントではないので、14年前に立てた入社目的を満足できたのかと、14年を経て自分の何が変わったのかを書いてみます。

私は誰?

手広く働いてきたジェネラリスト寄りのITエンジニアです。研究開発、性能改善、製品開発、要件発掘、品質保証、テクニカルライター、OSPO、セキュリティ、SREなどを色々やってきました。「何やってる人なんです?」と言われてうまく説明できた試しがありません。

OSSプロジェクトではクラスファイル解析ツールSpotBugsSLF4J向け静的解析ツールのメンテナ、actions/setup-javaのdependency cacheの実装もしています。

入社理由は満足できたのか

私が今の会社に入社した理由は3つありました。そしてそれらはこの14年間を通じて満足できたと思います。

  1. ユーザ数が多く、得られるフィードバックの質と量が期待できた
  2. 製品を持ち、それが社会に与える影響が大きいと思えた
  3. 経営陣が提唱する理屈・哲学に納得・共感できた

私は何がやりたいのか、あるいは何がやりたくないのかという話 - Kengo's blog

1について。ソフトウェア提供の形がパッケージソフトウェアからウェブアプリケーションに広がり、リーン開発手法の浸透やクラウドの普及、可観測性(Observability)技術などを通じてソフトウェア開発者が得られるフィードバックは大きく変化しています。一方でソフトウェアの向こうに人がいるのは変わらない事実であり、多くの顧客を抱えつつも個別の顧客と深く関わる機会もあるBtoBビジネスを経験できたのはとても幸運でした。顧客とのコミュニケーションを通じて得た経験は、アルゴリズムやデータ構造に関する知識のように今後のキャリアを長く支えてくれると思います。

2について。ソフトウェアには短いライフサイクルを持つものもありますが、私は自分が思っていた以上に長いライフサイクルを持つものが好きみたいです。レガシーと呼ばれるシステムもそうですし、FindBugsみたいなOSSもそうなんですが、長い実績を持つシステムに手を入れて品質を向上し長持ちさせるための工夫を考えて実行するのが性に合っていました。その点では請負開発やコンサルティングではなく、製品を抱えて育てるパッケージソフトウェアはとても適していたと思います。社会的課題の解決に貢献できている実感も定性的定量的に得られ、長期的にモチベーションを保つ支えになりました。

3について。創業経営者から色々学べたのは事実ですが、上司や同僚からも多くのことを学びました。これは当時の自分の想像を大きく越えた体験でしたし、「組織は人」という価値観を醸成するには充分すぎる体験でした。駐在員も経験し、世代や文化や価値観の違いに起因するコミュニケーションの難しさにも幾度となく直面しましたが、この多様性が多様な市場やコミュニティとの交流において価値を生むこともバズワードではなく体感として理解できた気がします。魚座の星占いでよく「清濁併せ呑む」ってワードが出てくるんですが、アレが組織の競争力と魅力の源泉として必要になる時代なのかなとか思います。

仕事に対する解像度が上がった話

この14年で趣味プログラマからプロプログラマ、そしてプロ開発者へと自己認識が変化したと感じています。

まず趣味プログラマからプロプログラマに自分の意識を変えることに、のべ2年はかかった気がします。プログラミングにおけるアマチュアとプロの大きな違いとしては例外処理や保守性、運用容易性への意識がよく語られます。私の場合は、コーディングをする際に機械だけではなく人にも目を向ける必要があるのだという気付きが大きなきっかけとなりました。コードを勝手にフォーマットしてコミットして迷惑を掛けるとか、コメント内容が古いシステムの発掘をするとか、なんでこんなログがここにあるんだと過去の自分に苛立つとか、めちゃくちゃ性能保守両面が考えられている実装に出会って感激するとか、そういう経験が大切でした。一応社会人になる前から他者のコードを読んだり自分のコードを公開したりはしていたはずなんですが、仕事として成果にコミットすることが私には学習効率を上げるために必要だったのでしょうか。フリーソフトなら「気に入らなきゃ使わなくていいよ」と言えてしまいますし、機能性や互換性よりも書き手の手間を減らすことに注力しがちだった気もします。

そして保守性や運用容易性に気を配るプロプログラマになれてさらに数年、自分の仕事がプログラミングでもシステム構築でもシステム運用でもない、課題解決なのだということに改めて向き合う精神的余裕が出てきました。業務におけるマネジメントの割合が増えてきたこととも無関係ではないと思います。

システム実装はミケランジェロが言う「余分なものを取り除くだけで理想の像が現れる」プロセスとは大きく異なります。似たシステムでもチームが違えば理想も変わりますし、要件が同じでも使える資源や技術が変われば自然と適用する技法も変わります。特に重要な特徴は、チームや資源、技術といったこれらの条件がすべて「時間」を変数としていること、つまり「常に正しい正解」が存在しないということでしょう。以前紹介したトリさんのスライドがとても参考になります。

よって「どのようなシステムを目指すのか」「何が余分なものなのか」をチームで議論することは依然重要ではありますが、それ以上に「よし!私達はこれで行く!プランBはこれ、プランCはこれ!いっちょやってみっか!」という自信と勢い、間違ったときに即時修正するための評価判断手段こそが必要です。そしてこれらは目標設定と戦略と迅速性によって、すなわちリーンな組織と情報公開(組織の可観測性)そして迅速に価値を届ける開発体制によって生み出されます。

この体制の実現はマネジメントとかリーダーシップとかソフトウェアエンジニアリングとかが噛み合うとてもおもしろい課題領域なのですが、この「課題と現状をすべて卓上に並べて明らかにし即時対応する」働き方はとても疲れるのですよね。GitLabのHandbookとかAmazonのWorking BackwardsとかGoogleのデザインドキュメントとか、コミュニケーションを円滑化かつ非同期化する手法はいろいろ知られているのですが、この「疲れる」ことに対する心理的抵抗感をいかに下げるか・チームとして越えるか、言い換えるといかに他のチームひいては顧客の信頼を獲得するかが大切なのだな、そしてプログラマやマネジメントとしての知識や技術そして自分自身の生き様が信頼獲得の武器になり得るのだなということが見えて、初めて「プロの開発者」になれた気がします。

まとめ

技術による社会課題の解決にこだわる会社に新卒入社して多くの学習の場を経験できたことが、自分の人生の大きな財産になりました。 関係各位におかれましては、至らぬところの多い私にお付き合いいただき、ありがとうございました。 新しい職場においても、自らの成長と社会課題の解決に向けて工夫して参ります。

オブジェクト指向か関数型か、という話題に私達はどう接するべきか

私がコードを書くときには「オブジェクト指向でいくか、それとも関数型か?」みたいなことはほとんど気にしていません。特にオブジェクト指向については人によって定義から違うこともままあるため、この手の議論がとても遠回りになることも多いと感じます。

ただきしださんのLT資料を拝見して、もしかしたらまだ需要があるのかなということで、この話題にどう接するべきか考えていることを書いてみます。

どう書くべきかはコンテキスト次第

結論から書くと、どのようにコードを書くべきかはチームや解決したい課題、利用言語や既存資産などのコンテキストによって変わります。 ので「何がオワコンでこれからは何が来る」みたいな議論は、チーム内という限られたスコープでのみ有効なはずです。 チームよりも広い場で議論する場合は、「どういったコンテキストにおいてどのような書き方をするか」のように若干抽象的なテーマが適切でしょう。

言い換えると、コードの書き方において絶対善や絶対悪は存在しないはずと考えています。例えばバッチ処理ないしウェブアプリケーションでは、複数スレッドから同一データを共有することで性能を高めるため、メソッドの戻り値をキャッシュしたりメモ化を施したりするかもしれません。このためにはデータが不変であると便利でしょう。 しかしこうした実装に登場するデータすべてが不変であるべきかというとそうではなく、むしろ可変データによって性能や可読性が向上することだってあるはずです。

私の経験した範囲でいうと、Repository内部で扱うデータを完全に不変にした結果コピーコンストラクタやシリアライズ・デシリアライズが頻出する読みにくいコードになったことがあります。 今ならライブラリの力を借りてビルダーを実装するなどもっとうまくできる気もしますが、単体テストによる品質担保を厚めにしつつ可変データを導入する手もあったはずでした。

トレンドの書き方が良いソフトウェアを届けるのに必須ということはない

OSSから例を出すと、OpenJDKやKotlin, Gradleといった著名なプロジェクトで使われているObjectWeb ASMはオブジェクト指向で書かれており、継承や配列といった今ならまず採用を避けるであろう書き方も頻出しています。また異なる意味を持つ intString も多く登場し、「この文字列はクラス名だっけティスクリプタだっけ?」といった注意を払いながらコードを読む必要があります。一部ではTypeTypePathみたいな型が用意されていて取り回すこともできますが、そのAPIはカプセル化やTellDontAskといった近年プログラマが慣れ親しんだものとは程遠いものです。

この面ではObjectWeb ASMは「プログラマの認知負荷を下げる」トレンドからは大きく離れていると言えます。

ですが、ObjectWeb ASMは事実上オワコンでなく、JVMエキスパートからの支持を集めて止まないわけです。 加速したJavaのバージョンアップにも速やかに追随し、コミュニティからの貢献を受け付けて修正をデリバリするとともに、リファクタリングや性能改善も行っています。今日SonarQubeを見たところではカバレッジ96.6%でした。まさに「質とスピード」を地で行くプロジェクトです。 *1

この面ではObjectWeb ASMは「ユーザに高い品質とかけがえのない価値を継続的に提供する」ソフトウェアの理想像に限りなく近いと言えます。 だいぶ極端な例ではありますが、トレンドの書き方が良いソフトウェアを届けるのに必須ということはないことを説明する良い事例だと思います。

曖昧な定義や由来を明らかにすることが重要

何がいいかはコンテキストによるのでコンテキストを明らかにしないまま議論をするのはやめましょう、というのが私の主張ではありますが、コンテキストを限定せずとも行うべき重要な議論・問題提起はあります。不明瞭な定義や由来に補足をしてただすものがそうです。言葉が曖昧だと議論が噛み合わず、建設的な議論になりません。例えばまさに今日読んだ Value Objectについて整理しよう - Software Transactional Memo はまさにこの貢献をするもので、とても勉強になりました。

定義や由来を明確にすることは、コンテキストが明らかなチームにおいても重要です。 例えばオブジェクト指向だと 2021年の「オブジェクト指向」を考える で指摘されるように、様々な定義が想起されます。 今話しているオブジェクト指向が何を意味しているのか、議論に参加する各々がきちんとすり合わせる必要があります。

まとめ

コードの書き方は結局、チームが望む働きを実現する道具のひとつです。チームの中で合意が取れているか、コードの理解と変更が容易か、APIや性能が利用者にも受け入れられているか……そういった要件に目を向けるべきです。オブジェクト指向や関数型も私達の道具箱に入っている道具のひとつとして、それぞれ尊重して理解につとめていきたいです。

*1:またAPIの認知負荷が高いのも、言い換えれば他のことに特化していると言えます。これは想像ですが、クラスファイルとプリミティブとの架け橋に徹し高い性能を実現することが設計の主目的かなと感じます。認知負荷については、各利用者が自分のコンテキストに最適化されたAPIを設計しそれでObjectWeb ASMを包むことで解決できます。ObjectWeb ASMはその用途が幅広いため、いたずらに抽象化してしまうと特定ユーザにとって使いにくいものになるでしょう。今のVisitorベースとTreeベースのAPIを提供するくらいがちょうどいいという判断かもしれません。

「非nullのint配列」をアノテーションで表すのは `@NonNull int[]` ではない

正解は int @NonNull [] です。な、なんだってー!

本当です。Java言語仕様書にも記載がありますが、配列を修飾する場合は [] の手前にアノテーションを書く必要があります。JVM仕様書に記載の例のほうがわかりやすいかもしれません:

@Foo String[][]   // Annotates the class type String
String @Foo [][]  // Annotates the array type String[][]
String[] @Foo []  // Annotates the array type String[]

組み合わせて考えると、「要素も配列自体も非nullのString配列」は @NonNull String @NonNull [] になります。コレクションは @NonNull List<@NonNull String> みたいにわかりやすいんですけどね。JavaのRecordでは配列を使わないほうが良いという話の時にも思いましたが、Javaは配列周りに非直感的な挙動が多い気がします。

なお配列だけでなく内部クラスでも同様で、パラメータが非nullの内部クラスを要求することをアノテーションで表現する場合は @NonNull Outer.Inner param ではなく Outer. @NonNull Inner param内部クラス名の手前にアノテーションを書く必要があります。これTwitterで11名にアンケートご協力いただいた限りでは、正答を答えられたのは1名だけという難問でした。

他にアノテーション周りで驚く機能としては、 this を修飾する方法も提供されています。receiver parameterと言います。Pythonのように第1引数にthisを書く形です:

class MyClass {
  void method(@Foo MyClass this, String param) {
    // ...
  }
}

いずれもJava8(2014年)からあった機能ですが、自分は今日まで知りませんでした。クラスファイルパーサを書くことが無ければ、このまま気づかなかったかもしれないです。Java、奥が深い。

リリース自動化の嬉しみとその手法

DevOpsやCIOps、GitOpsなどを通じて生産性向上を突き詰めていくと、コンパイルやテストだけではなくリリースまで自動したくなってきます。リリースには必要な作業が多く、また頻度も高くないため毎回思い出したり間違えたりが発生するためです。

特に変更内容をまとめて文書化する作業は、利用者に対する影響度もその煩雑さも高いため、自動化できれば文書の品質向上やリリース頻度の向上に大きく貢献できます。本記事では、筆者がNode/Java界隈でよく見るリリース自動化手法について紹介することで、リリース自動化の敷居を下げたいと思います。

なお本記事で言う「リリース」は、jarファイルやコンテナイメージなどビルドの成果物をリポジトリGitHub Releasesにアップロードして他プロジェクトやデプロイ環境で利用できるようにすることを指しています。環境に対する「デプロイ」や、エンドユーザへの公開を意味する「リリース」とは区別します。

自動化の前に

1. Changelogの要件を検討する

ソフトウェアのリリース時にその変更内容について説明する文書のことを、ChangelogまたはRelease Noteと呼びます。組織によってはChangelogとRelease Noteに異なる意味を持たせることもありますが、複数コミュニティから自動化手法を紹介する関係上、この記事では区別しません。

ChangelogにはKeep a Changelogというひろく知られた書式があります。自動化手法にはこの書式を意識したものも多いので、特に困らなければこれを採用します。

OSSでありがちなChangelogの保管場所としては、プロジェクトルートに CHANGELOG.md などのテキストファイルとして配置するか、GitHub Releasesの本文に記載する方法があります。Dependabotなどの依存管理手法はGitHub Releasesを参照して変更内容をユーザーに伝えるので、選択できるのであればOSSでなくともGitHub Releasesを使用することが望ましいでしょう。もちろんテキストファイルと併用しても構いませんし、GitHub PagesやWiki、チャットツールといった他の手法での公開も検討できます。

なおJava界隈ではChangelogに成果物のチェックサムを併記することもあります。これは利用者が成果物の整合性を検証する際に役立ちます。

自動化手法を選択する

1. Release Drafter

以下の記事にて紹介されているため詳細は割愛します。特徴はPRを使って開発しているプロジェクトであれば言語に依存せずほとんどのプロジェクトで採用できることでしょう。Maven開発のような非常に歴史の長いプロジェクトでも採用が検討されているようです

zenn.dev

2. GitHub ReleasesのRelease Note自動生成機能

GitHub Releases自体にも、Pull RequestをもとにRelease Noteを生成する機能が備わっています。 特に設定せずに導入が可能なため、とりあえず使ってみるには便利です。

docs.github.com

ラベルを使ってPRを分類するため、ラベルをPRに抜け漏れなく貼る運用が欲しくなるでしょう。actions/labelerのようなラベル管理を自動化する仕組みをあわせて検討すると良いかもしれません。

想定される利用手法は主に2つです:

  1. GitHub ReleasesからGUIを使ってリリースする手法。Releases作成時にGUI上に生成されるRelease Noteを目視確認できるため、安心して導入できるでしょう。Releasesのドラフトを作成→GitHub Actionsを発火し成果物をビルド→成果物をReleasesにアップロード→Releasesを公開 という流れです。
  2. CLIを使って自動化する手法。リリースプロセスから人手を廃するのに適しています。GitHub Actionsを発火し成果物をビルド→Releasesのドラフトをタグに対して作成、成果物をアップロード→Releasesを公開 という流れです。GitHub Actionsの発火にはタグ、あるいはリリースブランチへのpushを使うことが多いのではないでしょうか。

懸念があるとすれば、GitHubに対するロックインでしょうか。他の自動化手法と比べてバージョンが自動で決定されないのも特徴ですが、これはSemVer以外のバージョニングポリシー(例えばCalVer)を採用しやすいというメリットだと取ることも可能です。

3. semantic-release

SemVer2Conventional Commitsの利用を前提として、コミットメッセージをもとにリリースを自動実行する仕組みです。デフォルトブランチやリリースブランチに対するすべてのpushを契機としてリリースを行います。

semantic-release.gitbook.io

運用に柔軟性を持たせつつも、極力自動化し人の手を入れさせないための工夫が要所に見受けられるのが特徴です。なんせ、使っているロゴがこれです:

f:id:eller:20220216093216p:plain
「人間に作業させるとロクなことにならん」とか言ってそう

例えば運用には各開発者がConventional Commitsに従う必要がありますが、commitizenを使うことで導入障壁を下げることもできますし、commitlintを使うことでコミット時にコミットメッセージ書式の検証を行うこともできますし、semantic-pull-requestsを使うことでコミットメッセージが書式に従っていない場合にGitHub Checksを失敗させることもできます。またPRをsquash mergeすることで、コミットメッセージの決定をマージ時まで遅延することもできます。

またプラグイン機構による拡張も可能ですし、Shareable configurationsを使えば複数リポジトリをまたぐプロジェクトにも一貫した設定を行えますので、ある程度大きな規模の組織でも運用しやすいかもしれません。

利用方法はビルドのワークフローに npx semantic-release を埋め込むだけです。ブランチ名などの情報からリリースを行うべき状況だとsemantic-releaseが判断したら、Changelog生成やリリースが自動的に実行されます。リリースには npm publish./gradlew publish などのすでにコミュニティで利用されている手法が利用されるため、既存のリリース手順を再利用できます。

導入における主な課題は2つ。monorepoがbuilt-inではサポートされていないことと、Node.JS最新のLTSを必要とすることです。semantic-release自体はCI環境で実行するものなので開発者の手元にNode.JSを入れる必要はないのですが、前述のcommitizenやcommitlint, huskyといった関連ツールもほぼNode.JSコミュニティによって管理保守されているため、Node.JSを入れる判断をすることもあるでしょう。そのためNode.JS以外の環境を対象に開発しているプロジェクトではプロジェクトセットアップが若干複雑化するかもしれません。

なお似たものにstandard-versionrelease-itがあります。私は中の人を尊敬しスポンサーしているので、semantic-release推しです。spotbugs-gradle-pluginなど複数のOSSプロジェクトで使っていて、貢献受け入れやリリースを含め問題なく便利に回せています。

4. changesets

Atlassian発のmonorepoに特化した仕組みです。Node.JSを使ったmonorepoを開発しているのであれば検討しても良いかと思いますが、私はまだ試せていません。既存ユースケースもstandard-versionやsemantic-releaseと比べると1桁少ないです。

github.com

5. JReleaser

Node.JSではなくJVMを用いて動く仕組みです。他の仕組みと比べてまだ若いですが、既にMavenやGradleのサポートも用意されています。

jreleaser.org

f:id:eller:20220216093124p:plain
https://jreleaser.org/guide/latest/index.html より引用

Gradle用のクイックスタートを見た感じでは、 maven-publish プラグインではなく自前でリリース用の設定を持つようです。ここが ./gradlew publishnpm publish用の設定が完成されたプロジェクトに外付けする semantic-release とは大きく思想が異なる点です。コミュニティの進歩にJReleaserが自前でついていく必要があるため、保守コストが高くなる選択だと言えます。個人的には期待しつつもちょっと様子見です。

リリース自動化の果てに

1. 手動作業が残る部分

Changelog以外の文書は引き続き手動で作成する必要があります。例えば以下のようなものです:

  • エンドユーザ向けに変更内容を説明する文書
  • メジャーリリース時のマイグレーションガイド
  • マニュアル、プレス、その他

TwitterやSlackなどでの更新通知は自動化が可能ですが、もしブログ記事やメール通知のような手の込んだ文章を作成しているのであれば、それも残るでしょう。

ただこうした文書や通知はパッチリリース時にはあまり作らないはずで、パッチリリースの高速化・安定化・高頻度化は自動化によって充分に実現できると期待できます。

2. リリース自動化に向いたプロジェクト構成

CIやリリース自動化を推し進めると、ビルドやリリースに手作業が必要なプロジェクト構成はやりにくくなります。たとえば依存ライブラリを手でダウンロードしないとビルドできないとか、バージョン番号を手で書き換える必要があるとかです。

依存ライブラリについては、幸いJava界隈ではMaven Centralからほとんどのライブラリをダウンロードできます。昔では考えられなかったライブラリ、例えばOracle JDBC Driverもありますので、一度探してみるといいでしょう。
Maven Centralやその他のパブリックリポジトリに置いてないライブラリを使う場合は、自前でMaven Private Repositoryを管理してそこに置くことになるでしょう。これはNode.JSにおいても同様です。

バージョン番号の更新は、自動的に更新できるようにする必要があります。今回紹介した仕組みでサポートされているケースもありますし、Maven Release Pluginなどの機能を使うこともできます。ビルドツール設定以外のファイル、例えば META-INF/MANIFEST.MFBundle-Versionなどは手動ではなくビルドツールが自動で生成するようにしましょう。

3. リリース自動化に向いたブランチ戦略

リリース自動化は多くの場合、デフォルトブランチやリリースブランチが「常時リリース可能」であることを前提としています。 これは極力従うことが好ましいでしょう。

もしブランチが常時リリース可能でなかったら、リリース作業前に「リリース可能かどうか」を人間が検証する必要性が出てしまうためです。 もともと「自動化により人間の関与を減らしリリースの安定性と頻度を増やす」ことを目的に自動化しているのですから、ここに人間による作業を入れてしまうのは本末転倒です。少なくともリリース可能性検証のプロセスを自動化して、マージ前ビルドないしリリースビルドで自動的に検証されるようにするべきでしょう。

注意点として「マージしたらリリースされてしまう」ことを「完成するまでマージするべきではない」と受け止めない事が必要です。これはトピックブランチの寿命は短いほうが開発効率に良い影響があるためです。リリースできないとわかっている変更を公開することは避けつつ高頻度に変更をマージするために、Feature Toggleを使うなどの工夫が必要になるかもしれません。

まとめ

本記事では、筆者がNode/Java界隈でよく見るリリース自動化手法について紹介しました。

自動化手法 特徴 注意点
Release Drafter PRを使っていれば言語やビルドツールに関係なく利用可能。 リリースビルド用のワークフローを別に用意する必要がある。
GitHub ReleasesのRelease Note自動生成機能 設定不要。ラベルをもとにPRを分類してRelease Noteに反映。 バージョン番号を自分で決める必要がある。Release Note生成だけで、リリース作業自体は別に実行が必要。
semantic-release 柔軟な拡張性と徹底した自動化を両立。 実行にNode.JS最新のLTSが必要。標準ではmonorepoに非対応。
changesets 標準でmonorepoに対応。 Node.JSプロジェクト用。
JReleaser MavenやGradleといったビルドツールと統合。 JVM言語プロジェクト用。

リリース自動化はリリースの安定性と頻度を増やせる強力な仕組みです。ものによってはChangelogやコミットコメント、ブランチ戦略にリリース検証可能性検討の自動化といったものまで見直しをかける必要がありますが、それらも開発効率や生産性に寄与すると考えられているものがほとんどなので、開発体験向上のため検討してみてはいかがでしょうか。