gRPCの各言語向けパッケージ生成を一括管理する

この記事は [DeNA Advent Calendar 2021](https://qiita.com/advent-calendar/2021/dena) の10日目です

11/30にリリースした [PLAYBACK 9](https://playback9.jp/) では、クライアントとサーバーとのやり取りに gRPC を利用しています。
gRPCではIDLを利用してAPIを表現でき、クライアント及びサーバーのスタブコードを自動生成できるため非常に簡単に利用することができます。

しかしながら、生成したコードをどのように管理するかどうかは各自に委ねられており、ベストプラクティスが定まっているわけではありません。
インターネットでも様々な方法が公開されており、一番よく見かけるのは Git の Submodules を使い、共通の proto ファイルから各自コード生成を行うという方法です。

この方法は簡単ではありますが、

* コード生成の責務が各利用者に発生する
* 各言語で、どの時点のコードを利用しているか分かりづらい

といった問題があります。

この問題に対して、GitHub Actions と各言語で提供されているパッケージシステムを利用して解決します。
この解決方法により、クライアントおよびサーバーは通常のパッケージを利用するのと同じように生成されたコードを利用できるようになるため、コード生成の知識が必要ではなくなります。
また、セマンティックバージョニングにてバージョン指定するため、複数の環境があったとしてもバージョン番号によってどの時点で生成されたコードを利用しているかの確認が容易になります。

今回の例では、クライアントに TypeScript、サーバーに Go を利用して紹介していきます。
TypeScriptからサーバーへのアクセスには[gRPC Web](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md)を利用しています。

本記事で紹介するコードは以下のリポジトリにて公開しています。

https://github.com/amothic/sample-proto


## 生成されたコードを一括管理するリポジトリを作成する
最初に proto ファイルおよび各言語向けに生成されたコードを保存するリポジトリを作成します。
このリポジトリで、コード生成の責務を全て担うことになります。

リポジトリのディレクトリの構成は以下のようになります。
各言語向けに生成されたコードは各言語の名前の付いたディレクトリに置かれる形になります。
Go 向けに生成されたコードは、パッケージパスを`go`ではなく、`pb`にするため、さらに一階層ディレクトリを掘っています。


```
.
├── Makefile
├── README.md
├── doc
├── go
│   └── pb
├── proto
│   └── service.proto
└── ts
```

## コードの自動生成を行う

GitHub Actions を利用してコードが main ブランチに push された (PRがマージされた) タイミングでコード生成を常に行いコミットするように設定します。
これにより main ブランチでは、常に最新の生成されたコードが反映されていることを保証します。
`docker run` で自前の docker image を利用していますが、`protoc` が実行できるのであれば、どのイメージでも問題ないと思います。

https://github.com/amothic/docker-protoc

```.github/workflows/build.yml
name: Generate gRPC code
on:
  push:
    branches: main
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - name: Regenerate gRPC code
        run: |
          docker run --rm \
          -v `pwd`:/workdir -w /workdir \
          ghcr.io/amothic/protoc --proto_path=proto \
          --go_out=go/pb --go_opt=paths=source_relative \
          --go-grpc_out=go/pb --go-grpc_opt=paths=source_relative \
          --js_out=import_style=commonjs:ts --ts_out=service=grpc-web:ts \
          service.proto
      - name: Commit
        run: |
          [[ ! $(git diff --exit-code go ts) ]] && echo "Nothing to commit." && exit 0
          git config user.name "gRPC Bot"
          git pull
          git add go ts
          git commit -m "chore: regenerate grpc code"
          git push
```

## パッケージとしてリリースするための準備を行う
`go` ディレクトリと `ts` ディレクトリでパッケージとしてリリースするための準備を行います。

### Go
`go`ディレクトリ以下に新たに`go.mod`ファイルおよび`go.sum`ファイルを作成します。

```bash
$ go mod init github.com/OWNER/your-service-proto/go
$ go mod tidy
```

作成が終わったら、`commit`して`push`しておきます。

### TypeScript
`ts`ディレクトリ以下に新たに`package.json`ファイルを作成します。

```ts/package.json
{
  "name": "@OWNER/your-service-proto",
  "version": "0.0.0",
  "repository": "git@github.com:OWNER/your-service-proto.git"
}
```

必要なパッケージを追加し、`package-lock.json`を作成します

```bash
$ npm install --package-lock-only @improbable-eng/grpc-web google-protobuf @types/google-protobuf
```

また、github packageにアップロードするため以下の`.npmrc`ファイルも作成します

```ts/.npmrc
@OWNER:registry=https://npm.pkg.github.com
```

## セマンティックバージョンの更新を行う
更新するための最初のバージョンタグを作成します

```bash
$ git tag v0.0.1
$ git push origin v0.0.1
```

その後、バージョン番号を更新する GitHub Actions を追加します

https://github.com/kyoh86/git-vertag-action

こちらの GitHub Actions は、`major`, `minor`, `patch` のいずれかを渡すことで既存の Git Tag のバージョンをインクリメントして新たな Git Tag を打ってくれます。
`major`, `minor`, `patch` の文字列は、GitHub Actions の `workflow_dispatch` を利用して開発者が任意のタイミングで渡すこととします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/11027/80d96837-2f04-e746-bbc0-5c49b4bac9de.png)


```yml
name: release
on:
  workflow_dispatch:
    inputs:
      method:
        description: |
          Which number to increment in the semantic versioning.
          Set 'major', 'minor' or 'patch'.
        required: true
jobs:
  tag:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: kyoh86/git-vertag-action@v1.1
        with:
          method: ${{ github.event.inputs.method }}
          push: true
```

## バージョン番号を参照して、パッケージのリリース作業を行う
更新されたバージョンタグを利用して、各言語のリリース作業を行なっていきます。

### Go
Go のリリースは、通常Git Tag を打つだけなのですが、サブディレクトリに生成されたコードが置かれているので、サブディレクトリのパスを Tag に追加する必要があります

```yml
jobs:
  golang:
    needs: tag
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
        with:
          fetch-depth: 0
      # サブディレクトリにGoのモジュールがあるため、サブディレクトリのパスを先頭に付けたTagを追加で発行する
      - name: go module version tag
        run: |
          TAG_VERSION=$(git describe --tags --abbrev=0 --match='v*.*.*')
          MODULE_VERSION=go/$TAG_VERSION
          git tag $MODULE_VERSION
          git push origin $MODULE_VERSION
```

### TypeScript
Git Tag を参照して、`package.json` を更新し、リリース作業を実施します。
npm パッケージは、アクセスコントロールが容易な GitHub Packages に公開しています。

```yml
jobs:
  typescript:
    needs: tag
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
        with:
          fetch-depth: 0
      - name: ts package version tag
        id: version
        run: |
          PACKAGE_VERSION=$(git describe --tags --abbrev=0 --match='v*.*.*' | sed -e 's/v//')
          echo "::set-output name=package_version::${PACKAGE_VERSION}"
      - name: bump version
        run: |
          jq '.version|="${{ steps.version.outputs.package_version }}"' ts/package.json > tmp
          mv tmp ts/package.json
      - name: commit package.json
        run: |
          [[ ! $(git diff --exit-code ts/package.json) ]] && echo "Nothing to commit." && exit 0
          git config user.name "gRPC Bot"
          git pull
          git add ts/package.json
          git commit -m "chore: bump version to ${{ steps.version.outputs.package_version }}"
          git push
      - uses: actions/setup-node@v2
        with:
          node-version: '14'
          registry-url: 'https://npm.pkg.github.com'
          scope: '@OWNER'
      - run: npm publish
        working-directory: ./ts
        env:
          NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

## 生成したパッケージの利用
生成したパッケージを各言語で利用する方法について紹介します。

### Go
protoのディレクトリを`clone`できる状態なら、GitHubから直接取得することが可能です

```bash
// private repoの場合
$ export GOPRIVATE=github.com/OWNER

$ go get -u github.com/OWNER/your-service-proto/go
```

### TypeScript
1. [GitHub](https://github.com/settings/tokens) にて、`read:packages` の権限を与えた Personal Access Token を発行します (private repoの場合)
2. `npm login` (private repoの場合)

    ```bash
    $ npm login --scope=@OWNER --registry=https://npm.pkg.github.com
    
    > Username: USERNAME
    > Password: TOKEN
    > Email: PUBLIC-EMAIL-ADDRESS
   ```

3. `package.json`と同じディレクトリに`.npmrc`を作成します

    ```
    @OWNER:registry=https://npm.pkg.github.com
    ```
4. npm install

    ```bash
    $ npm install @OWNER/your-service-proto
    ```

[npmレジストリの利用 \- GitHub Docs](https://docs.github.com/ja/packages/working-with-a-github-packages-registry/working-with-the-npm-registry)

## 終わりに
GitHub Actions と各言語で提供されているパッケージシステムを利用して、生成されたコードをシンプルに利用できるようになりました。
他にも良い方法などあればコメントなどで指摘していただければ幸いです。

https://github.com/amothic/sample-proto