【Gradle】マルチモジュールプロジェクトのversion catalog移行・運用ルール制定を行った話

`Dependabot`が`version catalog`対応していたことを今更ながら知ったため、社内のマルチモジュール`SpringBoot`プロジェクトを`version catalog`移行しました。

https://www.docswell.com/s/nemuki/Z6Y3W7-2024-07-09-140132#p1

この記事では、移行に当たって意識したことや制定したルール、得られたメリットについて書きます。

# 移行で得られたメリット
当該プロジェクトでは基本的に`Spring`等の`BOM`に合わせたバージョンの依存を利用しており、移行で記述量が減らせる等のメリットはそれ程享受できなかった感が有ります。
ただ、バージョンを書く個所が一元化されたことによって、依存の更新作業を行う上で必要とされる前提知識を減らすことができ、作業ミスの発生確率もほぼ0にできました。
その意味で、完全な一元管理が可能というのはやはりメリットが大きいと感じます。

また、`Dependabot`が同じ依存に対しても複数の`PR`を送ってくる問題も解決されました[^dependabot]。

[^dependabot]: [既にグループ化して`PR`発行できるよう](https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#groups)なので、これは`Dependabot`設定が悪かっただけ感は有りますが……。

# 移行に当たって定めたルール
今後の運用において混乱が生じないよう定めた一応のルールに関しても書きます。
ただ、これに関しては走り出したばかりであるため、今後変動が有るかもしれません。

基本方針としては、以下を重視しました。

- 移行の労力を小さくする
- 追記が楽なようにする
- メンテナンスが楽なようにする

## `version catalog`への移行基準
少し触れましたが、この当該プロジェクトでは基本的に`Spring`等の`BOM`に合わせたバージョンの依存を利用しています。
このため、全ての依存を`version catalog`へ移行した場合、むしろ以下のようなデメリットが生じます。

- 移行に当たっての書き換え量が多くなる
- 依存を追加する場合`version catalog`と`dependencies`両方の書き替えが生じる
  - これは移行前より記述量が多い
  - 削除時も両方から削除のが必要になる 
- ルートの`version catalog`にあらゆる依存を詰め込んで見通しを悪くするか、ファイル分割で状況を複雑化させるかの択が生じる

そこで、移行対象はバージョンを明示的に指定しているものだけとし、以下のようにバージョン指定が省略されていれば移行対象外としました。

```kotlin:バージョン省略された依存の例
implementation("org.springframework.boot:spring-boot-starter-web")
```

## `version catalog`の記述ルール
### `[versions]`セクション
複数の依存で利用するか、`build.gradle`において定数的に利用したいような場合を除き、バージョンは直接書く方針としました。
これは日々の追記をコピペで済ませやすくするための方針です（後述）。

### `[plugins]`セクション
個々の定義は、基本的に以下の形式で書く方針としました。

```toml
[plugins]
spring-boot = { id = "org.springframework.boot", version = "3.3.2" }
```

これは[Gradle Plugins](https://plugins.gradle.org/plugin/org.springframework.boot)からのコピペをやりやすくするための方針です。
細かいことを言うと、形が似ているため、`Grooby`タブからコピペするのがお勧めです。

```gradle:Groovyタブに表示される例
plugins {
  id "org.springframework.boot" version "3.3.2"
}
```

試した限り以下のようなワンライナー形式でも書けるようでしたが、こちらは採用しませんでした。

```toml
[plugins]
spring-boot = "org.springframework.boot:3.3.2"
```

### `[libraries]`セクション
個々の定義は、基本的に以下の形式で書く方針としました。
この形式であっても`Dependabot`は正常稼働してくれることを確認しています。

```toml
[libraries]
spring-boot-starter-web = "org.springframework.boot:spring-boot-starter-web:3.3.2"
```

これは[Maven Central](https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web/3.3.2)からのコピペをやりやすくするための方針です。
細かいことを言うと、形が似ているため、`Gradle(Kotlin)`がコピペしやすかったです。

```kotlin:Gradle(Kotlin)タブに表示される例
implementation("org.springframework.boot:spring-boot-starter-web:3.3.2")
```

以下の形式でも書けます（というかこちらの方が一般的？）が、こちらは採用しませんでした。

```toml
[libraries]
spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web", version = "3.3.2" }
```

### その他
移行対象となるような内容は特別な事情を持っている場合が多く、付随するコメントも有りました。
それらのコメントに関しては、分散している方が混乱を招きそうだったため、同時に`version catalog`側へ移行することとしました。

また、以下の理由から、記述ルールは必要に応じて無視してOKという方針にしました。

- 日常的に頻出する操作が楽な方針にするとどうしても例外が出る
  - 特に`[libraries]`から`[versions]`に定義されたバージョンを参照する場合
- 方針的に`version catalog`はそれ程肥大せず、ロジックも無いため、多少のブレは許容できる

# ハマったポイント
最後におまけとして、移行時ハマったポイントを幾つか紹介します。

## `subprojects`内で`libs`に定義された内容が参照できない
以下に報告されている問題です。

https://github.com/gradle/gradle/issues/16634

これは一旦トップレベル変数に取得しておくことで解決できました。

```diff_kotlin
+ val foo = libs.foo.get()

  subprojects {
      dependencies {
-         implementation(libs.foo)
+         implementation(foo)
      }
  }
```

## 社内`GitHub Packages`の依存が`Dependabot`に検出されない
プロジェクトの`repositories`設定によって生じた問題です。
こちらは個別に記事にしています。

https://qiita.com/wrongwrong/items/9946d141ddf2bb599fed