CIのお供にGroovyもいいと思う

現在、サーバーサイド [Kotlin](https://qiita.com/search?sort=&q=tag%3AKotlin) を使ったプロジェクトの実装をしており、CIに [CircleCI](https://qiita.com/search?sort=&q=tag%3ACircleCI) を使っています。

CircleCIでは、各JobをDockerコンテナを使って動かせるので、そこでGroovyを使ってみてもいいかなと思って実践してみました。

# なぜGroovy？

例えば、ビルド前に以下のようなチェックを実施したいとします。

- [Flyway](https://flywaydb.org/) のマイグレーションファイルのバージョン番号が重複していないかチェックしたい

複数人で並行して開発していると、マージするタイミングによって、Flywayのマイグレーションファイルのバージョン番号が重複するかもしれません。 ~~（本当はちゃんとマージされる前に気づいてbumpしろって話ですが難しい）~~

もし間違って重複したままマージされても、デプロイされなければ（リポジトリは汚れるけど）動作の実害は起きないので、CIでビルド前・デプロイ前にチェックしたいです。

おそらく、どんなCIツールにしてもシェルスクリプトは使えるはずですが、シェルスクリプトで書くのはちょっとしんどい…。
そして、Kotlinを使ってるので、Java的言語ならみんな読めるはずだろう。
だけど、JavaやKotlinで書くと、ビルドしてクラスファイルを作って `java` コマンドで…とするほど大きな処理ではない。

そこでGroovyです。

# 例

実際に書いてみた例がこちらです。

:::note info
Flywayのマイグレーションファイルは、以下のフォーマットです。
V1.2.3.4__description.sql
:::

[SQL-based migrations](https://flywaydb.org/documentation/concepts/migrations.html#naming)

## 前提条件

- Flywayのマイグレーションファイルは、 `application/src/main/resources/db/migration` に保存されている
- チェック用のGroovyスクリプトは `devops` フォルダに保存されている
- JobでのGroovyの実行環境は、[オフィシャルのDockerコンテナ](https://hub.docker.com/_/groovy)を使う

## ソースコード

```groovy:devops/check_duplicated_flyway_migration.groovy
// マイグレーションファイルのパターン
def regexFileName = "^V([\\d+.])*\\d__.+\\.sql\$"

// ファイルが保存されているパスを指定
def directory = new File("${System.getProperty("user.dir")}/application/src/main/resources/db/migration")

// ファイル一覧を取得
def fileList = directory.listFiles()

// マイグレーションファイルのうち、バージョン番号でグループ化してカウント
def duplicatedVersions = fileList
        .findAll { it.name.matches(regexFileName) }
        .countBy { getVersion(it.name) }
        .findAll { it.value > 1 }

// もしリストが空でないならエラー
if (duplicatedVersions.size() > 0) {
    println("Error: Duplicated flyway migration found:")
    duplicatedVersions.keySet()
            .sort { sortByVersion(it) }
            .each {
                println("  $it")
                fileList.findAll { f -> f.name.startsWith(it) }
                        .collect { it.name }
                        .sort()
                        .each {println("    $it")}
            }

    // 終了コードを0以外にして、CIでエラー扱いとする
    System.exit(1)
} else {
    println("All flyway migration files have valid name:")
    fileList.findAll { it.name.matches(regexFileName) }
        .sort { sortByVersion(getVersion(it.name)) }
        .each { println("  ${it.name}") }
}

private static sortByVersion(String fileName) {
    def versionNumbers = fileName.replaceFirst("V", "")
    if (versionNumbers.length() == 0) {
        return []
    }

    versionNumbers.split("\\.").collect { Integer.parseInt(it) }
}

private static String getVersion(String fileName) {
    fileName.split("__", 2).first()
}
```

```yaml:.circleci/config.yml
version: 2.1

jobs:
  check-flyway-migration:
    docker:
      - image: groovy:4.0-alpine
    steps:
      - checkout
      - run: groovy devops/check_duplicated_flyway_migration.groovy
  test-build:
    steps:
      - checkout
      - echo テストしてビルドする
  deploy:
    steps:
      - echo デプロイする

workflows:
  version: 2
  build-deploy:
    jobs:
      - check-flyway-migration
      - test-build
      - deploy:
          requires:
             - check-flyway-migration
             - test-build
```


# 実際にCircleCIで動かしてみる

## 重複している場合

```
#!/bin/sh -eo pipefail
groovy devops/check_duplicated_flyway_migration.groovy
WARNING: Using incubator modules: jdk.incubator.vector, jdk.incubator.foreign
Error: Duplicated flyway migration found:
  V3.0.0.0
    V3.0.0.0__test1.sql
    V3.0.0.0__test2.sql
  V3.2.1.0
    V3.2.1.0__test5.sql
    V3.2.1.0__test6.sql
  V3.10.0.0
    V3.10.0.0__test3.sql
    V3.10.0.0__test4.sql

Exited with code exit status 1
CircleCI received exit code 1
```

これで、 `deploy` ジョブは動きません。

## OKの場合
```
#!/bin/sh -eo pipefail
groovy devops/check_duplicated_flyway_migration.groovy
WARNING: Using incubator modules: jdk.incubator.foreign, jdk.incubator.vector
All flyway migration files have valid name:
  ここにファイル名

CircleCI received exit code 0
```

これで、 `deploy` ジョブが動きます（ `test-build` も成功している場合）。

---

このように、シェルスクリプトで書くのは難しい、PythonとかNodeとか知らない、PythonやNodeだとvenvとかpackage.jsonとか色々…という場合、Groovyを使うこともアリかなと感じました。