Kengo's blog

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

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やコミットコメント、ブランチ戦略にリリース検証可能性検討の自動化といったものまで見直しをかける必要がありますが、それらも開発効率や生産性に寄与すると考えられているものがほとんどなので、開発体験向上のため検討してみてはいかがでしょうか。

Gradle/Kotlinで開発する私的ベストプラクティス2022

こちらのエントリーが素敵だなと思ったので、最近書いてるKotlinプロジェクトのベストプラクティスをまとめてみます。一部はJavaプロジェクトにおいても利用できるはずです。

zenn.dev

基本方針

  • 参加障壁を下げる。OSSプロジェクトでもプロプライエタリ・ソフトウェアプロジェクトでも、新しい開発者が参加するコストを下げることには大きな意義がある。
  • 環境差異を吸収する。javaにPATHが通ってさえいればOSに関係なくビルドが通るようにする。
  • プロジェクト固有ルールを作らない。Conventional CommitsやKeep a changelogなど、ひろく世に使われているルールを採用する。

Gradleを設定する

Spotlessを使う

コードのフォーマットはformatterに任せて人間は細かいことを考えない、というのが不特定多数が参加するソフトウェアプロジェクトのあるべき姿だと考えています。ここを妙にこだわるとエディタ縛りだとかタブ幅だとかのいわゆる”地雷”の多い話題を避けて通れませんし、プロジェクト固有のルールができて敷居が高くなり保守コストが高くなるという課題もあります。

逆に言えば、フォーマットをツールに任せれば、人間はエディタやOSの選択の自由を享受できます。

Gradleの場合、最もシームレスに使えるツールはSpotlessでしょう。MarkdownとKotlin、Kotlin Buildscriptすべてのフォーマットをこのツールで管理できます。

github.com

pre-merge buildでは spotlessCheck タスクを使ってフォーマットを確認します。このタスクは通常 check タスクから依存されていますので、深いことを考えずに ./gradlew build すれば充分です。

Git hookを導入する

commit時にSpotlessを自動実行するには、git hookを使います。git hookの設定にはghooksを使っていたのですが、脆弱性対応含む更新が止まっており*1使いにくい状態です。後述するsemantic-releaseを使うのであれば、NodeJSにPATHが通っている前提でhuskyを使っても良いでしょう。

JVM toolchainを使う

現時点でJavaには8,11,17という3つのLTSリリースがあります。誰でも簡単にビルドできるプロジェクトを作るためには、どのバージョンにJAVA_HOME環境変数が通っていても問題なくビルドできるべきです。このためのアプローチには「Java 8で動くようにする」と「JVM toolchainを使う」の2択があります。

既にJava 8をサポートしていないGoogle Errorproneやサポート終了を予告しているApache Camelのようなツールもありますので、JVM toolchainを使って「JAVA_HOMEがJava 8を指していても常にJava 11あるいは17を使ってビルドする」方が現時点でしょう。最新のKotlinプラグインならtoolchainの利用は容易です:

kotlin {
    jvmToolchain {
        (this as JavaToolchainSpec).languageVersion.set(JavaLanguageVersion.of(17))
    }
}

.gitattributes を書く

GitHubを使っている場合、人間に差分をレビューさせたくないファイルがあれば linguist-generated=true を指定しておきます。

./gradlew init で生成されるファイルを参考に、Windows向けの改行コード設定 *.bat text eol=crlf を加えても良いでしょう。

*.bat text eol=crlf
gradlew linguist-generated=true
gradlew.bat linguist-generated=true

settings.gradle.kts を書く

公式ドキュメントで言及されているとおり、設定ファイルをプロジェクトルートに置くことが強く推奨されています。 ./gradlew initでプロジェクトを作っていれば自動的に作成されているはずです。

最低限rootProject.nameを設定しておきます。これでどのようなフォルダ名が使われていても同じプロジェクト名になります。 またビルドスキャンをする予定があるなら、このタイミングで設定をしておくと良いでしょう。

plugins { id("com.gradle.enterprise") version "3.8.1" }

rootProject.name = "foo-bar"

gradleEnterprise {
  buildScan {
    termsOfServiceUrl = "https://gradle.com/terms-of-service"
    termsOfServiceAgree = "yes"
  }
}

gradle.properties を書く

公式ドキュメントをもとに設定しておきます。Java併用時に google-java-format を使う場合、少なくとも Java 17では--add-exportsの指定が必要です。

org.gradle.caching=true
org.gradle.configureondemand=true
org.gradle.jvmargs=-XX:+HeapDumpOnOutOfMemoryError -Xmx1G --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
org.gradle.parallel=true

ドキュメンテーションコメントにはDokkaを使う

私はあまり語れるほど使っていないのですが、KotlinプロジェクトではKDocをドキュメンテーションコメントに使い、KDocからの文書生成にDokkaを使います。設定も単純なので特に迷うことなく使えるはずです。

github.com

build タスクに必要なタスクすべてを実行させる

Gradleプロジェクトでは基本的に、 ./gradlew build によってプロジェクトのビルドと検証に必要なタスクがすべて実行されるべきです。assemble タスクによって成果物が作られ、 check タスクによってすべてのテストや検証が実行される状態を保つことで、これを実現できます。これらのタスクの役割は ライフサイクルタスクとしてドキュメントに説明されています。

ただSpotlessを使っている場合、手元では spotlessApply も実行したいがCI環境ではspotlessApply を実行させたくない、というケースがあるかもしれません。

これにはDSLによりCI環境の場合に分岐する、CI環境では -x オプションにより特定タスクをスキップする、手元で ./gradlew spotlessApply を明示的に実行させる、などの手法があります。プロジェクト固有ルールを作らないという方針と、標準では build タスクが spotlessApply タスクに依存していない現状を踏まえると、手元で ./gradlew spotlessApply を明示的に実行するのが良いと考えています。

とはいえ人間に「push前に./gradlew spotlessApplyを実行してね」というプロジェクト固有ルールを作りたくはないので、前述のGit hookに実行させる方針を採ることになるでしょう。

GitHub Actionsを設定する

ごく一般的なワークフロー設定は以下のようになります。 なおWindowsでビルドする場合は、--no-daemonオプションでGradle daemonを無効化する必要があります

jobs:
  build:
    strategy:
      matrix:
        os: ['windows-latest', 'ubuntu-latest']
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v2
      - uses: gradle/wrapper-validation-action@v1
      - run: ./gradlew build --no-daemon
      - uses: actions/upload-artifact@v2
        if: always()
        with:
          name: reports (${{ matrix.os }})
          path: build/reports

Dependabotを設定する

現時点では buildSrc プロジェクトの依存は更新対象に入っていません。以下のように明示的に指定する必要があります。

version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "daily"
    commit-message:
      prefix: "build"
  - package-ecosystem: "gradle"
    directory: "/"
    schedule:
      interval: "daily"
    commit-message:
      prefix: "fix"
  - package-ecosystem: "gradle"
    directory: "/buildSrc"
    schedule:
      interval: "daily"
    commit-message:
      prefix: "build"

逆に言えば、ビルドに用いるツールの依存を buildSrc プロジェクトに集めることで、ビルドに用いるツールとプロジェクトそのものの依存とを分けて扱うことができます。上記設定ではプロジェクトそのものの依存にのみ fix 接頭辞を用いることで、semantic-releaseによるリリースを発火させています。

semantic-release を使う

以下の記事で以前紹介したので詳細は省きます。なお今なら拙作GradleプラグインがあるのでGradleでもsemantic-releaseのフル機能を用いて開発できます。

blog.kengo-toda.jp

PATHにNode最新のLTSが通っている状態にする必要がありますが、これはnvmやasdfを使うことになるでしょう。最新のactions/setup-node@v2はnode-version-fileでファイルからNodeのバージョンを読み込めます:

- uses: actions/setup-node@v2
  with:
    node-version-file: '.nvmrc'
    cache: 'npm'
- run: |
     npm ci
     npx semantic-release

以上です。なおJava用ではありますが、ビルドの細かい話やGitHub Actionsの使い方については以下の本にも書いてますのでご参考まで。

zenn.dev

*1:PRは作ったんですが見てもらえてなさそう