🤖

GitHub Actionsでのcodegen & PR作成でOpenAPIの変更を楽々レビューする

2023/10/09に公開

概要

バックエンドのリポジトリにあるOpenAPI定義が変更されたときに、OpenAPIクライアントコードを生成して、フロントエンドのリポジトリに対してPRを自動作成するGitHub Actionsを作成しました。

モチベーション

バックエンド側でOpenAPIを変更したPRが上がってきたら、生成されるクライアントコードの差分も見てレビューしたかった。
requiredの付け忘れやnullableなenumの見落としだったり、使用しているライブラリによる自動生成コードの挙動だったりを、yamlファイルだけ眺めてレビューするのは自分には難しかったです。
なので毎回手元でBE, FEのリポジトリをチェックアウトして、コマンド叩いて...とやってたんですが、流石に面倒なので自動化しようと思いました。

最初は差分をgithub-commentで表示する方法を考えたのですが、生成されたコードもgit管理しているので結局PRを作成する必要があること、typecheckを始めとするCIが回る恩恵があることを理由に、PRを自動で作成する手法をとりました。

実装

ざっくり流れは以下のフローチャートの通りです。
Repository DispatchでFEのGitHub Actionsをトリガーしています。

FE側のworkflow

name: Create Codegen PR
on:
  repository_dispatch:
    types:
      - codegen

env:
  node-version: '18'
  pnpm-version: '8'

jobs:
  create-pr:
    runs-on: ubuntu-latest
    timeout-minutes: 5
    steps:
      - name: Parse Payload
        run: |
            echo "PR_NUMBER=${{ github.event.client_payload.pr_number }}" >> $GITHUB_ENV

      - name: Checkout BE
        uses: actions/checkout@v3
        with:
          repository: your-org/backend-repo
          path: backend
          token: ${{ secrets.GH_PAT }}

      - name: Checkout BE Branch
        run: |
          gh pr checkout $PR_NUMBER
        working-directory: backend
        env:
          GITHUB_TOKEN: ${{ secrets.GH_PAT }}

      - name: Checkout Web
        uses: actions/checkout@v3
        with:
          path: frontend

      - name: Check PR Already Exists
        run: |
          echo "PR_EXISTS=$(gh pr list | grep chore/be-$PR_NUMBER | wc -l)" >> $GITHUB_ENV
        working-directory: frontend
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Checkout Web Branch
        run: |
          if [ $PR_EXISTS -eq 1 ]; then
            gh pr checkout chore/be-$PR_NUMBER
          else
            git checkout -b chore/be-$PR_NUMBER
          fi
        working-directory: frontend
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - uses: pnpm/action-setup@v2
        with:
          version: ${{ env.pnpm-version }}

      - uses: actions/setup-node@v3
        with:
          node-version: ${{ env.node-version }}
          cache: 'pnpm'
          cache-dependency-path: 'frontend/pnpm-lock.yaml'

      - uses: actions/cache@v3
        id: pnpm-cache
        with:
          path: 'frontend/node_modules'
          key: ${{ runner.os }}-pnpm-${{ hashFiles(format('{0}/frontend{1}', github.workspace, '/pnpm-lock.yaml')) }}
          restore-keys: |
            ${{ runner.os }}-pnpm-

      - name: Install dependencies
        if: steps.pnpm-cache.outputs.cache-hit != 'true'
        run: pnpm install
        working-directory: frontend
        env:
          NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Codegen from OpenAPI
        run: |
          pnpm codegen
          echo "CODEGEN_DIFF=$(git diff --shortstat)" >> $GITHUB_ENV
        working-directory: frontend

      - name: Commit Codegen Diff
        if: ${{ env.CODEGEN_DIFF }}
        run: |
          git config user.name github-actions[bot]
          git config user.email 41898282+github-actions[bot]@users.noreply.github.com
          git add .
          git commit -m "your commit message"
          git push origin HEAD
        working-directory: frontend

      - name: Create PR
        if: ${{ env.CODEGEN_DIFF && env.PR_EXISTS == 0 }}
        run: |
          gh pr create --title "your pr title" \
            --body "your pr body" \
            --base main \
            --head chore/be-$PR_NUMBER
        working-directory: frontend
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

雑な補足

  • BEのリポジトリがprivateなので、チェックアウト時にPATを利用しています
  • FE側のコード生成のconfigが、こんな感じでopenapi.yamlを参照するようになっているので、それに従ってcheckoutしています
.
├── frontend/
│   └── codegen.config.json
└── backend/
    └── docs/
        └── openapi.yaml
  • pnpmを利用しているので、dependenciesのインストールとキャッシュのステップが入ってます
  • commitはgithub-actions[bot]ユーザーとして行っています
  • FE側のブランチはchore/be-{BE側のPR番号}という命名にしています

BE側のworkflow

curlでRepository Dispatchをトリガーするだけになっています。
FE側でPR作成後に、BEのPRにリンクを貼るみたいなのもやりたかったんですが、PRの本文でメンションしておけば十分かなと思って割愛しました。

PR Mention by bot

name: Dispatch Frontend Workflow
on:
  pull_request:
    types:
      - opened
      - synchronize
    paths:
      - docs/openapi.yaml

jobs:
  codegen-diff:
    runs-on: ubuntu-latest
    steps:
      - run: |
          curl -X POST https://api.github.com/repos/your-org/your-repo/dispatches \
            -H "Accept: application/vnd.github+json" \
            -H "Authorization: Bearer ${{ secrets.GH_PAT }}" \
            --data '{"event_type": "codegen", "client_payload": { "pr_number": ${{ github.event.number }} }}'

workflowの境界

FE側のコード生成をする都合上がっつりFEの環境構築のstepが発生するので、BE側のworkflowはOpenAPIの変更を検知してFE側に通知するだけに留めました。

最低限要件として、PRの作成のstepはFE側のworkflowで行う必要があります。
異なるプライベートリポジトリへPRを出す場合はPersonal Access Tokenを使わなければならず、そうするとPRの作成者がPATの発行者になってしまうためです。
FE側でstepを実行すれば、GITHUB_TOKENの権限でPRが作成できるので、botのアカウントがPRを作成してくれます。

PR by github-actions(bot)

場合によってはBE側で完結するようにしたほうがGitHub Actionsの料金とかはお得になったりするかもしれません。

おわり!

Discussion