やること
- OpenAPI-Specification v3に準拠したAPI定義ファイル(yaml or json形式)
- OpenAPIGeneratorのカスタマイズ用リポジトリ
上記2つに変更を加えることは、OpenAPIGeneratorで生成されるコードにも変更が生じます。
その変更を都度人間が手動で対応するのではなく、GithubActionsでコード生成用のコマンドを走らせ、変更の反映を自動化させます。
用意するリポジトリ
- API定義ファイルを管理するリポジトリ
- 今回 sample-openapi という名前でサンプルを用意しました。
- API定義ファイルの例は本家のpetstore.yaml を借用しています。
- v3前提なので、まだv2の方は[コンバーターでv3にしましょう。](- v2→v3コンバーター https://mermade.org.uk/openapi-converter)
- 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にワークフローを追加
全体図
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に変更があった場合のみ発火する設定
on:
push:
paths:
- "swagger.yaml"
- "openapi-generator"
- サブモジュールを有効にする設定(プライベートリポジトリの場合はこちらを参照してください。)
- name: submodule
uses: actions/checkout@v1
with:
submodules: true
- openapi-generatorのビルド
- 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 }}
アクション
全体図
name: "code_gen"
description: "SwaggerからOpenAPIGeneratorを用いてコード自動生成"
branding:
icon: "gift"
color: "green"
runs:
using: "docker"
image: "Dockerfile"
FROM adoptopenjdk/openjdk8:alpine-slim
RUN apk add --no-cache git
COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/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ファイルのパスを変数化
export JAR_PATH=./openapi-generator/modules/openapi-generator-cli/target/openapi-generator-cli.jar
- validateコマンドでバリデーション
java -jar ${JAR_PATH} validate -i swagger.yaml
- 生成前に古い生成物を削除
rm -rf kotlin-client kotlin-server swift typescript go-gin
- generateコマンドで各種言語のコードを生成
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
# 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
リポジトリでやってしまいます。
ワークフロー
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ファイルを置きます。
#!/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の設定をします。
#!/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でやるのがうまくいかず一旦挫けました、、
後書き
長々と書きましたが、コードを読めば単純なことをしているので、是非リポジトリを参照してみてください。
スキーマファースト・スキーマ駆動という言葉が大分浸透してきた昨今ですが、個人的にここ最近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
- スキーマファースト開発のススメ