LoginSignup
8

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-12-21

やること

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

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

用意するリポジトリ

  1. API定義ファイルを管理するリポジトリ
  2. OpenAPIGeneratorをカスタマイズしたフォーク or ミラーリングしたリポジトリ
    • 今回の例では私がフォークしたu-nation/openapi-generatorを使用します。
    • 本家をそのまま使ってもいいですが、今回はカスタマイズする前提でフォークした場合の例を紹介します。

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

方法1

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

ssh方式でもhttps方式でも大丈夫です。プライベートリポジトリを追加する場合は下記リンクを参照してください。
GithubActionsでプライベートリポジトリをsubmoduleとして取り込む

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にワークフローを追加

全体図

.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に変更があった場合のみ発火する設定
.github/workflows/code_gen.yml
on:
  push:
    paths:
      - "swagger.yaml"
      - "openapi-generator"
  • サブモジュールを有効にする設定(プライベートリポジトリの場合はこちらを参照してください。)
.github/workflows/code_gen.yml
      - name: submodule
        uses: actions/checkout@v1
        with:
          submodules: true
  • openapi-generatorのビルド
.github/workflows/code_gen.yml
      - name: Build OpenAPIGenerator
        run: |
          cd openapi-generator
          ./mvnw clean package -DskipTests=true
  • 自作のアクションを実行
.github/workflows/code_gen.yml
      - name: swagger
        uses: ./.github/actions/code-gen
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

アクション

全体図

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

RUN apk add --no-cache git

COPY entrypoint.sh /entrypoint.sh

ENTRYPOINT ["/entrypoint.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ファイルのパスを変数化
./github/actions/entrypoint.sh
export JAR_PATH=./openapi-generator/modules/openapi-generator-cli/target/openapi-generator-cli.jar
  • validateコマンドでバリデーション
./github/actions/entrypoint.sh
java -jar ${JAR_PATH} validate -i swagger.yaml
  • 生成前に古い生成物を削除
./github/actions/entrypoint.sh
rm -rf kotlin-client kotlin-server swift typescript go-gin
  • generateコマンドで各種言語のコードを生成
./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
./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 リポジトリでやってしまいます。

ワークフロー

.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ファイルを置きます。
./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の設定をします。
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:

後書き

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

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

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

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

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

参考記事

注釈

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
8