Platformグループでマネージャーをしている松田( @tadamatu ) です。
この記事に書いてあること
GitHub Actions を利用し 「OpenAPI の自動バージョニング」から「API Clientのnpmパッケージ生成」までを完全自動化 したのですが、その際に ハマったこと、工夫したこと が結構あったので、シェアしておきたいと思い書かせていただいた記事になります。
具体的には以下のような内容について書いてあります。
- Branch protection rulesを維持した状態で、workflowからだけはcommitをさせたい(bypass機能を利用) → 文中の(3-2)
- 別ブランチの GitHub packages に
npm publish
したい(通常は何もしなければGitHub Actionsからは同じリポジトリのGitHub packagesにしかnpm publish
できない)→ 文中の(2) - Branch protection rulesを維持した状態で、PRが生成されたときに Auto Merge されるようにしたい → 文中の(4)
特にbypass機能に関しては、GitHub Community
で出されていたリクエストが今年の5月に利用できるようになったようで、早速利用してみたものになります。
そもそも何を解決したかったのか(目的)
PlatformグループではBASE全体のリアーキテクチャを進めているのですが、その課題の1つに BackendとFrontendの分離 というものがあります。
現在はOpenAPIを採用しているのですが、 WebAPIスキーマの管理とClientの生成フロー を適切にしたいと言うのが目的でした。
※これまではBackendとFrontendが同じリポジトリ内にあったため、リポジトリ内部でよしなに生成し融通を利かせて開発を行っていたのですが、物理的にリポジトリが別れるとそれが難しくなるためです。
最終的には以下のような構造とし、期待値は以下のようなものです。
- スキーマ定義ファイル
openapi.yaml
はBackend側のリポジトリにて管理をする- WebAPIスキーマ自体はFrontend・Backend開発者の合意で決定されるが、Backendの開発者が素案を作る文化が強いため、それに合わせてチューニングした仕組みとした
- (もちろんモックサーバをたててFrontend開発したりする場合もある)
openapi.yaml
を修正すると、GitHub Actionsを通してClientコードを生成しnpmパッケージとして自動リリースする- フロントエンド開発者は
npm install
で簡単にClientコードを取得できる - ほとんどはmasterへのpushトリガーで処理されるが、Clientコードが生成されるまでフロントエンド開発者が開発することができないため、手動で開発用パッケージを生成できるようにして解決している
- 今回はバージョンもワークフロー内で自動採番しており作業衝突が回避できる
- フロントエンド開発者は
- 「
openapi.yaml
のバージョン番号」と 「npm package
のバージョン番号」を一致させる- これによりBackendとFrontend が同じスキーマを利用していることが明確になり、Frontendもpackage.jsonを見るだけで判断できるようになる
全体フロー
全体フローはこのような感じになっています。
のマークの付いているところが、マニュアル操作 の部分になります。
それ以外の部分はワークフロー(GitHub Actions)により処理が行われます。
【開発中フェーズ】
- (1) スキーマ定義ファイル
openapi.yaml
を実装して開発ブランチ へ push - (2) 開発用のnpmパッケージ出力
- → (a) Generate Client for Dev
openapi-generetor
によりopenapi-client (typescript-fetch)
生成- TypeScriptトランスパイル
- 開発用パッケージ公開
- 開発中はこの開発用パッケージを利用してFrontend側の開発を行います
- → (a) Generate Client for Dev
【本番リリース】
本番リリースのため masterブランチへの merge(push) をトリガーにGithub Actionsが実行されたあとは、以下のような処理が全て自動で処理されます。
- (3) 本番リリース(merge)
- → (b) Update Version
- (3-1) 新しいバージョン番号の発行(
major
/minor
/patch
のコントロール含む) - (3-2)
backendリポジトリ(selfリポジトリ)
のopenapi.yaml
を新しいバージョン番号に更新しコミット - (3-3)
openapi-clientリポジトリ
へPR生成package.json
を新しいバージョン番号へ更新openapi-generetor
によりopenapi-client (typescript-fetch)
生成
- (3-1) 新しいバージョン番号の発行(
- → (b) Update Version
- (4) (c) Auto merge(PR自動マージ)
- 生成されたPRの自動マージ
- (5) (d) Build & Publish package(正式版パッケージ公開処理)
- TypeScriptトランスパイル
- 正式版パッケージ公開
- 新しいパッケージが生成されたことをslackへ通知
以降に各処理を詳しく書いていきます。
注意)
・以降に出てくるコードは、ブログ用にコメントを追加し、適時編集したものです。ご了承ください。
・BASEでは適切な単位で openapi.yaml
が複数存在するのですが、以降はcartをサンプルにしたものですので、適時読み替えてください。
事前準備
GitHub Actions では通常、実行開始時に GITHUB_TOKEN が発行され、${{ secrets.GITHUB_TOKEN }}
に保存されたTokenを利用して基本的な操作をすることが可能です。
自動トークン認証 - GitHub Docs
しかし、リポジトリを跨いでActionしたり、今回のようにbypass機能を利用したい場合は、GITHUB_TOKEN ではActionできません。
リポジトリを跨いだActionなどは PAT(Personal Access Token) でも可能です。
個人用アクセス トークンを管理する - GitHub Docs
しかし、PATは個人に依存してしまいますので、組織配下で作成した GitHub Apps を利用し生成したToken を利用した方が良いでしょう。
また、今回のbypass機能もGitHub Appsでないと機能しません。
GitHub Appsを作成
では、GitHub Appsを作成して、インストールしていきます。
(a) GitHub Appsを作成
Organizationページ から [Settings]
- [Developer settings]
- [GitHub Apps]
- [New GitHub Apps]
と進み...
以下のように入力し、[Create GitHub App]
クリックで GitHub Apps を作成します。
# 今回の対応で変更が必要な部分だけ記載 GitHub App name: BASE openapi client generated App Homepage URL: https://binc.jp/ (適当で大丈夫) Webhook-Active: 不要なのでoff Contents=Access: Read and Write Pull requests=Access: Read and Write
(b) 作成したAppページで以下の情報を拾う
- 作成したAppページに表示されている 【App ID】
[Generate a private key]
押下しprivate key
を生成
その時にダウンロードされる 【pemファイル】
(c) 必要なリポジトリにインストール
作成したAppページの左メニュー[Install App]
から、今回利用するリポジトリ backendリポジトリ
, openapi-clientリポジトリ
で有効になるようにインストールする
(d) 各リポジトリページで Sercretsを入力
[Settings]
- [Sercrets]
- [Actions]
と移動し、(b)で取得した 【App ID】 と 【pemファイル】の内容 を設定します。
この記事では以下のように設定し、workflowから利用しています。
OPENAPI_CLIENT_GENERATED_APP_ID: 【App ID】 OPENAPI_CLIENT_GENERATED_PRIVATE_KEY: 【pemファイル】の内容
(1) スキーマ定義ファイルを実装して開発ブランチへ push
この章では赤枠の部分を説明しています
まずはスキーマ定義ファイル openapi.yaml
をローカルで実装します。
ここは言わずもがなだと思いますので省略しますが、API仕様に合わせて以下のようなyamlファイルを修正しGitHubへpushします。
openapi-generator/samples/yaml/pet.yml at master · OpenAPITools/openapi-generator · GitHub
1つ注意が必要なのは、このタイミングで openapi.yaml
のversionを実装時に変更しない ということです。
同時に別の開発者によってスキーマ修正が行われている可能性もありますし、hotfixで急遽変更になる場合もあります。
あくまで 本番デプロイ時にバージョンが自動的に割り当てする ことで、バージョン番号の調整にいちいちコミュニケーションを必要としなくてもよい、というメリットが得られます。
今回のフローでは (3-1) でバージョン番号が発行されます。
(2) 開発用のnpmパッケージ出力
この章では赤枠の部分を説明しています
(1)でpushされた openapi.yaml
をもとにFrontendで開発するためには、 pushされたスキーマ定義のWebAPIクライアントパッケージをFrontendにインストールする必要があります。
今回の場合、パッケージを生成するためには、pushされたブランチを指定し 「 (a) Generate Client for Dev」 をマニュアル実行することで開発用のnpmパッケージファイル が生成されます。
このときバージョン番号は更新されません。
npmパッケージにはbeta版やrc版といった任意のプレリリース識別子
をつけることができます。
また、同じプレリリース識別子でもbuild番号
をその後ろにつけることができます。
例) ex. 1.2.0-rc.8
(識別子=rc
, build番号=8
)
config | npm Docs
今回はdev版として「識別子=commit short_sha
,build番号=日時
」として、いつのどの時点のpublishしたものかを理解しやすくしました。
# 例: <version>-<short_sha>.<yyyymmddhhmm>
1.2.3-de0b012.202209050312
また、公開のコマンドでは npm publish --tag dev
とし、latestバージョンが移動しないようにもしています。
workflowの中で、トランスパイルやパッケージ公開をしていますが、 package.json
tsconfig.json
の設定に対して特殊な設定はしていないので、今回は省略します。
#### (a)Generate-Client-for-Dev #### name: "OpenAPI: Generate Client For Dev" on: workflow_dispatch: jobs: generate-openapi-client: runs-on: ubuntu-latest steps: # selfリポジトリ以外の取得に利用するため GitHub Apps の Token を取得 - name: Generate apps token id: generate_token uses: tibdex/github-app-token@v1 with: app_id: ${{ secrets.OPENAPI_CLIENT_GENERATED_APP_ID }} private_key: ${{ secrets.OPENAPI_CLIENT_GENERATED_PRIVATE_KEY }} # backendリポジトリ(selfリポジトリ) のチェックアウト (openapi.yamlのありか) - name: Checkout Current Repogitory uses: actions/checkout@v2 # openapi-client リポジトリのチェックアウト (パッケージをpublishするリポジトリ) - name: Checkout Client Repogitory uses: actions/checkout@v2 with: token: ${{ steps.generate_token.outputs.token }} repository: baseinc/openapi-client path: client # nodeセットアップ - name: Setup node uses: actions/setup-node@v1 with: node-version: 16.x registry-url: https://npm.pkg.github.com # yqセットアップ (openapi.yamlの分析に利用) - name: Setup yq uses: chrisdickinson/setup-yq@latest # 開発用パッケージバージョン作成のための情報を取得 - name: Get vars id: vars run: | echo "::set-output name=api_version::`yq r './openapi.yaml' 'info.version'`" echo "::set-output name=sha_short::$(git rev-parse --short HEAD)" echo "::set-output name=dev_version::`date +"%Y%m%d%I%M"`" # https://openapi-generator.tech/docs/usage/#generate - name: Generate OpenAPI Client for typescript-fetch uses: openapi-generators/openapitools-generator-action@v1 with: generator: typescript-fetch generator-tag: latest command-args: | -i=openapi.yaml \ -o=client/packages/typescript-fetch/cart/src/ \ --generate-alias-as-model \ --enable-post-process-file # npm install と TypeScriptトランスパイル - name: install node_modules & build run: | cd client/packages/typescript-fetch/cart npm install npx tsc # NODE_AUTH_TOKEN には backend リポジトリの GITHUB_TOKEN を使用している # backend から openapi-client リポジトリの package へ `npm publish` することになるが、 # これをするには package setting より Manage Actions access の設定に backend を設定する必要がある - name: Publish npm package for DEV run: | VERSION=${{ steps.vars.outputs.api_version }} cd client/packages/typescript-fetch/${{ github.event.inputs.target }} npm version ${VERSION}-${{ steps.vars.outputs.sha_short }}.${{ steps.vars.outputs.dev_version }} npm publish --tag dev env: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ここでのはまりポイントとしては、BASEではpackageの管理をGitHub pakages で行っているのですが、実行されるworkflowがbakendリポジトリ なのに対し、 publish先が openapi-clientリポジトリ となっており、通常は単純にpublishはできません。(これはTokenに PAT
や GitHub Apps
を利用してもダメでした)
これをするためには、GitHub pakages のリポジトリ (今回の場合はopenapi-clientリポジトリ)に、Manage Actions access という設定がありますので、そこで backendリポジトリ からの書き込みを許可します。
こうすることで、backendリポジトリのGITHUB_TOKEN
で publishすることが可能 になります。
(3) 本番リリース
この章では赤枠の部分を説明しています
openapi.yaml
が確定し、開発が完了すると本番にリリース(masterブランチにmerge)をします。
merge(pushトリガー)により 「 (b) Update Version」 が実行されて (3-1)(3-2)(3-3) が処理されます。
(3-1) 新しいバージョン番号の発行
この章では赤枠の部分を説明しています
ここでは 新しいバージョン番号の発行 を行います。
特別な処理がなければ、通常はpatchバージョンを更新します。
minor, major バージョンを更新 するためには、 merge対象のcommit comment のprefix に [openapi/major]
[openapi/minor]
を入れておく と該当のバージョンを更新してくれます。
複数ある場合は、一番直近のprefixが有効になります。
#### (b)Update-Version-<1> #### name: "OpenAPI: [Cart] Update Version" on: push: branches: - master paths: - "openapi.yaml" # 複数回連続実行された場合、直列に実行されて、最後のワークフローが有効になるようにconcurrencyを設定 concurrency: ${{ github.workflow }} jobs: update-openapi-version-cart: runs-on: ubuntu-latest # selfリポジトリ に対してコミットを追加するため、対象のコミットコメントの場合、workflowは無視する if: ${{ !startsWith(github.event.head_commit.message, 'chore(release):') }} steps: # backendリポジトリをチェックアウト - name: Checkout Current Repogitory uses: actions/checkout@v2 # nodeセットアップ - name: Setup node uses: actions/setup-node@v1 with: node-version: 16.x registry-url: https://npm.pkg.github.com # yqセットアップ (openapi.yamlの分析に利用) - name: Setup yq uses: chrisdickinson/setup-yq@latest # 現在のバージョン番号を取得(例. current_version = 1.2.3) - name: Get vars id: vars run: | echo "::set-output name=current_version::`yq r openapi.yaml 'info.version'`" # 基本はpatchバージョンが更新されます # commit comment を遡り、prefix に特定の文字列があれば minor や major バージョンを更新 - name: Get version type uses: actions/github-script@v6 id: version-type with: script: | const commits = ${{ toJSON(github.event.commits) }} for (const commit of commits.reverse()) { if (commit.message.startsWith('[openapi/major]')) return 'major' if (commit.message.startsWith('[openapi/minor]')) return 'minor' if (commit.message.startsWith('[openapi/patch]')) return 'patch' } return 'patch' result-encoding: string # patchバージョンを更新(例. 1.2.3 → 1.2.4) - name: Version Up (patch) if: steps.version-type.outputs.result == 'patch' run: | VERSION=${{ steps.vars.outputs.current_version }} && a=( ${VERSION//./ } ) && a[2]=$((a[2] + 1)) echo "new_version=${a[0]}.${a[1]}.${a[2]}" >> $GITHUB_ENV # minorバージョンを更新(例. 1.2.3 → 1.3.0) - name: Version Up (minor) if: steps.version-type.outputs.result == 'minor' run: | VERSION=${{ steps.vars.outputs.current_version }} && a=( ${VERSION//./ } ) && a[1]=$((a[1] + 1)) echo "new_version=${a[0]}.${a[1]}.0" >> $GITHUB_ENV # majorバージョンを更新(例. 1.2.3 → 2.0.0) - name: Version Up (major) if: steps.version-type.outputs.result == 'major' run: | VERSION=${{ steps.vars.outputs.current_version }} && a=( ${VERSION//./ } ) && a[0]=$((a[0] + 1)) echo "new_version=${a[0]}.0.0" >> $GITHUB_ENV # (b)Update-Version-<2> に続く ==>
(3-2) backendブランチ(selfブランチ)の openapi.yaml を新しいバージョン番号に更新しコミット(bypass機能を利用)
この章では赤枠の部分を説明しています
(3-1)で発行されたバージョン番号を、backendブランチ(selfブランチ)の openapi.yaml
へ更新する(書き戻す)必要があります。
しかし、ここで問題になるのは Branch protection です。「masterブランチへの直接push禁止」「1人以上Approveしていないとマージできない」などといったあれです。
workflowで何も気にせずにコミットをしても、通常はこのBranch protectionに弾かれてエラーになってしまいます。
しかし bypass機能 を利用することで、 特定のGitHub Apps からの命令をbypassして実行させる ことができます。(つまり、Branch protectionをスルーしてコミットができるようになります)
設定は簡単で、GitHub Apps を設定してあれば、bypassさせたいリポジトリ(今回の場合はbackendリポジトリ)に設定するだけです。
[Settings] - [Branchs] - [Branch protection rules] と進み、Protect matching branches
内の Allow specified actors to bypass required pull requests
をONにし、該当する GitHub Apps
を追加します。
これにより、人による誤操作はBranch protectionによって守りつつ、該当する GitHub Apps
のワークフローでだけ実行したいアクションができるようになりました。
以下のワークフローで実際に利用しています。
#### (b)Update-Version-<2> #### # ==> (b)Update-Version-<1> の続き # openapi.yaml のバージョンを上書きする - name: OpenAPI Versioning run: | sed -i -e "s/version: ${{ steps.vars.outputs.current_version }}/version: ${{ env.new_version }}/" openapi.yaml # GitHub Apps は branch protection を bypass する設定することで、commit を master に直pushすることができる # まずは GitHub Apps のtoken を取得 - name: Generate apps token id: generate_token uses: tibdex/github-app-token@v1 with: app_id: ${{ secrets.OPENAPI_CLIENT_GENERATED_APP_ID }} private_key: ${{ secrets.OPENAPI_CLIENT_GENERATED_PRIVATE_KEY }} # 次に backendリポジトリ(selfリポジトリ) へ commit - name: Commit New Version to baseinc/backend repo uses: stefanzweifel/git-auto-commit-action@v4 with: commit_message: "chore(release): openapi.yaml update version from ${{ steps.vars.outputs.current_version }} to ${{ env.new_version }}" github-token: ${{ steps.generate_token.outputs.token }} # (b)Update-Version-<3> に続く ==>
(3-3) openapi-clientリポジトリへPR自動生成
この章では赤枠の部分を説明しています
以下のワークフローでは、openapi-clientリポジトリに対し、新しいスキーマ定義のクライアントのPRを自動生成 します。
同時に、(3-1)で発行されたバージョン番号を、packageにも反映させています。
#### (b)Update-Version-<3> #### # ==> (b)Update-Version-<2> の続き # PR生成のために、openapi-client をチェックアウト - name: Checkout Client Repogitory uses: actions/checkout@v2 with: token: ${{ steps.generate_token.outputs.token }} repository: baseinc/openapi-client path: client # https://openapi-generator.tech/docs/usage/#generate - name: OpenAPI Generate typescript-fetch uses: openapi-generators/openapitools-generator-action@v1 with: generator: typescript-fetch generator-tag: latest command-args: | -i=openapi.yaml \ -o=client/packages/typescript-fetch/cart/src/ \ --generate-alias-as-model \ --enable-post-process-file # openapi.yaml で更新したバージョン番号と同じものを、package.json に対して適用する - name: Update release version run: | cd client/packages/typescript-fetch/cart npm version ${{ env.new_version }} # PRを生成する # [automerge] というprefixをcommit commentにつけることで、次の工程(4)で自動マージが発火する - name: Create pull request to baseinc/openapi-client repo uses: peter-evans/create-pull-request@v4 with: token: ${{ steps.generate_token.outputs.token }} path: client delete-branch: true commit-message: "chore(release): npm package update version from ${{ steps.vars.outputs.current_version }} to ${{ env.new_version }}" title: "[automerge] chore(release): npm package update version from ${{ steps.vars.outputs.current_version }} to ${{ env.new_version }}" body: | openapi-typescript-fetch-cart: ${{ env.new_version }} branch: chore/openapi-client-cart branch-suffix: short-commit-hash # 同じプルリクは作らない
(4) PRの作成によりAuto Mergeされる
この章では赤枠の部分を説明しています
openapi-clientリポジトリ側でPRが生成されたら、以下のworkflowが発火します。
このworkflowでは [automerge]
というprefix がついているときだけ、自動マージが行われるようになっています。
AutoMerge には、GitHubのAutoMerge機能を利用しています。
AutoMerge機能 は[Settings]
- [General]
の Allow auto-merge
をONにすることで、利用ができます。
これを利用する利点は、全てのcheckが通った時にだけ発火することです。たとえば万が一CIでエラーになれば、MergeされずにPRは残ることになります。
Branch protection には「マージするには1人以上のApproveが必要」が設定してあり、workflow内でApproveをするようにしてあります。
これにより、 人による誤操作はBranch protectionによって守りつつ、workflowによる自動マージだけが動作する ようにしてあります。
#### (c)Auto-Merge #### name: Auto Merge on: pull_request: types: - opened branches: - main jobs: release: name: Auto Merge runs-on: ubuntu-latest # `[automerge]` というprefixがある場合にだけこの workflow が発動する if: ${{ startsWith(github.event.pull_request.title, '[automerge]') }} steps: # GitHub Apps により該当PRの GitHub AutoMerge を ON にする - name: Generate apps token id: generate_token uses: tibdex/github-app-token@v1 with: app_id: ${{ secrets.OPENAPI_CLIENT_GENERATED_APP_ID }} private_key: ${{ secrets.OPENAPI_CLIENT_GENERATED_PRIVATE_KEY }} - name: Enable Pull Request Automerge uses: peter-evans/enable-pull-request-automerge@v2 with: token: ${{ steps.generate_token.outputs.token }} pull-request-number: ${{ github.event.number }} # AutoMerge 条件(つまり branch protection)は「1人以上のApproveが必要」であるため、 # Approve する → これにより GitHub Auto Merge が発動 - name: Approve uses: hmarr/auto-approve-action@v2 with: pull-request-number: ${{ github.event.number }}
(5) openapi-clientを生成し、npmパッケージを生成
この章では赤枠の部分を説明しています
最後の工程として、(4)で自動マージされると、以下のworkflowがpushトリガーにより発火します。
ここでは、TypeScriptトランスパイルをした後、パッケージとして公開する といった作業をしているだけになります。
#### (d)Build-&-Publish-package #### name: Publish packages on: push: branches: - main jobs: release: name: Build and Publish package runs-on: ubuntu-latest steps: # openapi-clientリポジトリ をチェックアウト - name: checkout uses: actions/checkout@v2 # nodeセットアップ - name: setup node uses: actions/setup-node@v1 with: node-version: 16.x registry-url: https://npm.pkg.github.com # npm install と TypeScriptトランスパイルをした後、パッケージとして公開 - name: build & publish package (cart) id: publish_package_cart run: | cd packages/typescript-fetch/cart npm install npx tsc npm publish ./ echo ::set-output name=version::$(npm version --json | jq '."@baseinc/openapi-typescript-fetch-cart"') env: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # 生成完了をslackに通知 - name: Success Slack Notification if: contains(steps.changed-files.outputs.all_changed_files, 'packages/typescript-fetch/cart/package.json') uses: rtCamp/action-slack-notify@v2 env: SLACK_MESSAGE: "新しい openapi-typescript-fetch-cart: ${{ steps.publish_package_cart.outputs.version }} が生成されました" SLACK_TITLE: "openapi-client 生成通知" SLACK_USERNAME: "openapi-client-generated-bot" SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_CART }}
まとめ
今回は、アーキテクチャ変更に合わせて、OpenAPI Client 生成の自動化した話 を、ワークフローを中心に書いてきました。
GitHub Actions は本当に良くできている と思います。
こんなことできないかなと Marketplace を検索してみると、大体のものは見つかったりします。
これからも作業効率を上げるために、また人による誤操作をできるだけ減らす 意味も含め、GitHub Actions を使い倒していきたいと考えています。
長文を最後までお読みいただき、ありがとうございました。
最後に
私が所属する Platformグループ は、今年の7月に新設されたばかりのグループで、仲間を絶賛募集中です!
Platformグループでは、 「オーナーやカスタマーの良い体験、エンジニアの開発体験をプラットフォームで守る」というビジョン のもと、リアーキテクチャをメインに、作業効率化やパフォーマンス改善、組織との親和性について考えるといった作業まで、様々な改善を行っています。
もしご興味あれば、カジュアル面談も実施しておりますので、ぜひお気軽にお問い合わせください。