GithubActionsでOpenAPIGeneratorの生成機能を自動化する

## やること

1. OpenAPI-Specification v3に準拠したAPI定義ファイル(yaml or json形式)
2. OpenAPIGeneratorのカスタマイズ用リポジトリ

上記２つに変更を加えることは、OpenAPIGeneratorで生成されるコードにも変更が生じます。
その変更を都度人間が手動で対応するのではなく、GithubActionsでコード生成用のコマンドを走らせ、変更の反映を自動化させます。

## 用意するリポジトリ
1. API定義ファイルを管理するリポジトリ
    - 今回 [sample-openapi](https://github.com/u-nation/sample-openapi/blob/master/petstore.yaml) という名前でサンプルを用意しました。
    - API定義ファイルの例は本家の[petstore.yaml](https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v3.0/petstore.yaml) を借用しています。
    - v3前提なので、まだv2の方は[コンバーターでv3にしましょう。](- v2→v3コンバーター https://mermade.org.uk/openapi-converter)
2. OpenAPIGeneratorをカスタマイズしたフォーク or ミラーリングしたリポジトリ
    - 今回の例では私がフォークした[u-nation/openapi-generator](https://github.com/u-nation/openapi-generator)を使用します。
    - 本家をそのまま使ってもいいですが、今回はカスタマイズする前提でフォークした場合の例を紹介します。

上記2つのリポジトリにそれぞれGithubActionsを適応していきます。
以降の説明ではAPI定義ファイルを管理するリポジトリを**sample-openapi**、OpenAPIGeneratorをカスタマイズしたフォーク or ミラーリングしたリポジトリを**openapi-generator**と表記するので適宜読み換えて頂ければと思います。

## 方法１

### `sample-openapi`にopenapi-generatorをサブモジュールとして追加

**ssh**方式でも**https**方式でも大丈夫です。プライベートリポジトリを追加する場合は下記リンクを参照してください。
[GithubActionsでプライベートリポジトリをsubmoduleとして取り込む](https://qiita.com/u_nation/items/8ff79a9831c05f3f1d1d)

```
cd sample-openapi
# どちらかの方法で追加する
## https方式
git submodule add https://github.com/u-nation/openapi-generator.git
## ssh方式
git submodule add git@github.com:u-nation/openapi-generator.git

git submodule update --init
```


サブモジュール追加後は、GithubActionsを作成するまえにmasterブランチに取り込んでおく必要があります。

### sample-openapiにワークフローを追加

**全体図**

```yaml:.github/workflows/code_gen.yml
name: code_gen

on:
  push:
    paths:
      - "swagger.yaml"
      - "openapi-generator"

env:
  DOCKER_BUILDKIT: 1

jobs:
  generate:
    runs-on: ubuntu-latest
    steps:
      - name: submodule
        uses: actions/checkout@v1
        with:
          submodules: true
      - name: Set up JDK 8
        uses: actions/setup-java@v1
        with:
          java-version: 8
      - uses: actions/cache@v1
        with:
          path: ~/.m2/repository
          key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
          restore-keys: |
            ${{ runner.os }}-maven-
      - name: Build OpenAPIGenerator
        run: |
          cd openapi-generator
          ./mvnw clean package -DskipTests=true
      - name: swagger
        uses: ./.github/actions/code-gen
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

#### ワークフロー解説
- swagger.yamlとopenapi-generatorに変更があった場合のみ発火する設定

```yaml:.github/workflows/code_gen.yml
on:
  push:
    paths:
      - "swagger.yaml"
      - "openapi-generator"
```

- サブモジュールを有効にする設定(プライベートリポジトリの場合は[こちら](https://qiita.com/u_nation/items/8ff79a9831c05f3f1d1d)を参照してください。)

```yaml:.github/workflows/code_gen.yml
      - name: submodule
        uses: actions/checkout@v1
        with:
          submodules: true
```

- openapi-generatorのビルド

```yaml:.github/workflows/code_gen.yml
      - name: Build OpenAPIGenerator
        run: |
          cd openapi-generator
          ./mvnw clean package -DskipTests=true
```

- 自作のアクションを実行

```yaml:.github/workflows/code_gen.yml
      - name: swagger
        uses: ./.github/actions/code-gen
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

#### アクション

**全体図**

```yaml:./github/actions/action.yml
name: "code_gen"
description: "SwaggerからOpenAPIGeneratorを用いてコード自動生成"
branding:
  icon: "gift"
  color: "green"
runs:
  using: "docker"
  image: "Dockerfile"
```

```dockerfile:./github/actions/Dockerfile
FROM adoptopenjdk/openjdk8:alpine-slim

RUN apk add --no-cache git

COPY entrypoint.sh /entrypoint.sh

ENTRYPOINT ["/entrypoint.sh"]
```

```sh:./github/actions/entrypoint.sh
#!/bin/sh
set -eu

# git setting
git config --global user.name github-actions
git config --global user.email noreply@github.com

export GIT_BRANCH="$(git symbolic-ref HEAD --short 2>/dev/null)"
if [ "$GIT_BRANCH" = "" ] ; then
  GIT_BRANCH="$(git branch -a --contains HEAD | sed -n 2p | awk '{ printf $1 }')";
  export GIT_BRANCH=${GIT_BRANCH#remotes/origin/};
fi

git remote set-url origin https://u-nation:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git
git checkout $GIT_BRANCH

# OpenAPI Generate
export JAR_PATH=./pkg-openapi-generator/code-gen/openapi-generator-cli.jar
## validation
java -jar ${JAR_PATH} validate -i swagger.yaml
## remove old generated code
rm -rf kotlin-client kotlin-server swift typescript go-gin
## generate kotlin-server
java -jar ${JAR_PATH} generate -i swagger.yaml -g kotlin-spring --additional-properties useBeanValidation=true --enable-post-process-file -o kotlin-server
rm -rf kotlin-server/docs \
  kotlin-server/build.gradle.kts \
  kotlin-server/pom.xml \
  kotlin-server/settings.gradle \
  kotlin-server/src/main/resources \
  kotlin-server/src/main/kotlin/org/openapitools/api \
  kotlin-server/src/main/kotlin/org/openapitools/Application.kt \
  kotlin-server/src/test
## generate kotlin-client
java -jar ${JAR_PATH} generate -i swagger.yaml -g kotlin -o kotlin-client
rm -rf kotlin-client/docs kotlin-client/src/main/kotlin/org/openapitools/client/infrastructure
## generate swift
java -jar ${JAR_PATH} generate -i swagger.yaml -g swift4 -o swift
## generate go
java -jar ${JAR_PATH} generate -i swagger.yaml -g go-gin-server -o go-gin
## generate typescript
java -jar ${JAR_PATH} generate -i swagger.yaml -g typescript-axios -o typescript
## remove all README.md
find kotlin-client kotlin-server swift typescript go-gin -name "README.md" | xargs rm

# push if diff exists
set +e
git diff --exit-code --quiet
if [ $? -eq 1 ]; then
  git add .
  git commit -m "add generated code"
  git push origin $GIT_BRANCH
else
  echo "nothing to commit"
fi
```

#### アクション(entrypoint.sh)解説
- ワークフローでビルドされたjarファイルのパスを変数化

```sh:./github/actions/entrypoint.sh
export JAR_PATH=./openapi-generator/modules/openapi-generator-cli/target/openapi-generator-cli.jar
```

- validateコマンドでバリデーション

```sh:./github/actions/entrypoint.sh
java -jar ${JAR_PATH} validate -i swagger.yaml
```

- 生成前に古い生成物を削除

```sh:./github/actions/entrypoint.sh
rm -rf kotlin-client kotlin-server swift typescript go-gin
```

- generateコマンドで各種言語のコードを生成

```sh:./github/actions/entrypoint.sh
java -jar ${JAR_PATH} generate -i swagger.yaml -g kotlin-spring --additional-properties useBeanValidation=true --enable-post-process-file -o kotlin-server
java -jar ${JAR_PATH} generate -i swagger.yaml -g kotlin -o kotlin-client
java -jar ${JAR_PATH} generate -i swagger.yaml -g swift4 -o swift
java -jar ${JAR_PATH} generate -i swagger.yaml -g go-gin-server -o go-gin
java -jar ${JAR_PATH} generate -i swagger.yaml -g typescript-axios -o typescript
```

- 生成物をリポジトリの同じブランチにpush

```sh:./github/actions/entrypoint.sh
# git setting
git config --global user.name github-actions
git config --global user.email noreply@github.com

export GIT_BRANCH="$(git symbolic-ref HEAD --short 2>/dev/null)"
if [ "$GIT_BRANCH" = "" ] ; then
  GIT_BRANCH="$(git branch -a --contains HEAD | sed -n 2p | awk '{ printf $1 }')";
  export GIT_BRANCH=${GIT_BRANCH#remotes/origin/};
fi

git remote set-url origin https://u-nation:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git
git checkout $GIT_BRANCH

~略~

# push if diff exists
set +e
git diff --exit-code --quiet
if [ $? -eq 1 ]; then
  git add .
  git commit -m "add generated code"
  git push origin $GIT_BRANCH
else
  echo "nothing to commit"
fi
```

## 方法2

### `openapi-generator`にGithubActionsを設定する
- `sample-openapi`でやっていた `./mvnw clean package -DskipTests=true` コマンドでjarファイルを生成していたのを `openapi-generator` リポジトリでやってしまいます。

#### ワークフロー

```yaml:.github/workflows/push.yml
name: openapi-generator-cli

on:
  push:
    paths-ignore:
      - "openapi-generator-cli.jar"

env:
  DOCKER_BUILDKIT: 1

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@master
      - name: Set up JDK 8
        uses: actions/setup-java@v1
        with:
          java-version: 8
      - uses: actions/cache@v1
        with:
          path: ~/.m2/repository
          key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
          restore-keys: |
            ${{ runner.os }}-maven-
      - name: Build OpenAPIGenerator
        run: ./mvnw clean package -DskipTests=true
      - name: Git push
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: ./.github/actions/entrypoint.sh
```

#### アクション

- `openapi-generator` リポジトリに`code-gen`ディレクトリを生成して、そのディレクトリに生成されたjarファイルを置きます。

```sh:./github/actions/entrypoint.sh
#!/bin/sh
set -eu

# git setting
git config --global user.name github-actions
git config --global user.email noreply@github.com

export GIT_BRANCH="$(git symbolic-ref HEAD --short 2>/dev/null)"
if [ "$GIT_BRANCH" = "" ] ; then
  GIT_BRANCH="$(git branch -a --contains HEAD | sed -n 2p | awk '{ printf $1 }')";
  export GIT_BRANCH=${GIT_BRANCH#remotes/origin/};
fi

git remote set-url origin https://u-nation:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git
git checkout $GIT_BRANCH

# replace old jar
rm -f ./code-gen/openapi-generator-cli.jar
cp -f ./modules/openapi-generator-cli/target/openapi-generator-cli.jar ./code-gen/openapi-generator-cli.jar

# push if diff exists
set +e
git diff --exit-code --quiet
if [ $? -eq 1 ]; then
  git add .
  git commit -m "add generated code"
  git push origin $GIT_BRANCH
else
  echo "nothing to commit"
fi
```

### `sample-openapi`にsparse-checkoutの設定をする
- `sample-openapi`はサブモジュールとして、`openapi-generator`全てを取り込んでいましたが、依存するものはjarファイルのみなので、`./code-gen/openapi-generator-cli.jar`だけを取り込むようにsparse-checkoutの設定をします。

```sh:git-sparse-checkout.sh
#!/bin/sh

git submodule update --init

for module in $( ls ./.git/modules ); do
	echo "set $module to sparse-checkout"
	cd ${module}
	git config core.sparsecheckout true
	cd ../
	echo '/code-gen' > ./.git/modules/${module}/info/sparse-checkout
	cd ${module}
	git read-tree -mu master
	cd ../
done
```

### 方法2のメリット

- `openapi-generator`のカスタマイズは一度完成すれば変更頻度は少ないので、`sample-openapi`で毎回`./mvnw clean package -DskipTests=true`する手間が省ける
- なので、かなり早い（`方法1`:**2m25s前後**に対して`方法2`:**50秒前後**）

### 方法2のデメリット
- 21MBくらいあるjarファイルをGitリポジトリに突っ込んでしまう、、

## 方法3

### TODO: GitHub Package Registryを使う
- `openapi-generator`のGithubActionsで、自身でつける任意のバージョンが変更された場合だけビルドしたjarファイルをGitHub Package Registryに登録
- `sample-openapi`はGitHub Package Registryに登録されたjarファイルをダウンロードして使う
- サブモジュールを使う必要がなくなるので、多分これが一番いいです(>_<)
- が、 `mvn deploy` をGithubActionsでやるのがうまくいかず一旦挫けました、、:bow::sob:

### 後書き
長々と書きましたが、コードを読めば単純なことをしているので、是非リポジトリを参照してみてください。

- https://github.com/u-nation/openapi-generator
- https://github.com/u-nation/sample-openapi

スキーマファースト・スキーマ駆動という言葉が大分浸透してきた昨今ですが、個人的にここ最近API定義と実装に乖離があるメンテされてないAPI定義を沢山見てきました、、🤮
定義通りのリクエスト・レスポンスがされないので、定義の信用が地に落ちてすぐに形骸化される傾向があります。。💀

OpenAPIGeneratorを使用しスキーマ駆動開発を実践すると、人間が書くコード量が減るメリットはもちろんのこと

- API定義と実装の乖離がなくなるので、フロントエンド・バックエンドの疎通がスムーズになる
- モック用のJsonを作っておけばバックエンドの開発を待たずにフロントの実装が進められる
- 実装がAPI定義に依存するので、API定義の恒常的なメンテを強制できる

というメリットがあるので、今後も活用していきたいです。

#### 参考記事
- https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line#using-a-token-on-the-command-line
- https://help.github.com/en/actions/automating-your-workflow-with-github-actions/authenticating-with-the-github_token
- https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets
- [スキーマファースト開発のススメ](https://blog.onk.ninja/2017/09/21/schema_first_development)

#### 注釈

[^1]: GithubActionsのv1時代はプライベートリポジトリでも `GITHUB_TOKEN` で事足りてた気がする。。