GoのMulti-module repositoryとバージョン管理

現在仕事で開発しているサービスはmonorepoでMulti-module repository構成を取っています。最近はmonorepoで2年くらい開発していて慣れると快適ですが、運用(特にCIとか)は考えないといけない点がいくつかあります。今回はその中でもGoでMulti-module repositoryするときに直面した課題について紹介します。

## Multi-module repositoryとは

公式FAQ( https://github.com/golang/go/wiki/Modules#faqs--multi-module-repositories )に書いてあることが全てですが、リポジトリ内に複数の `go.mod` が存在している状態を指します。

```
my-repo
|-- bar
|-- foo
|   |-- rop
|   `-- yut
|-- go.mod
`-- mig
    |-- go.mod
    `-- vub
```

monorepoで開発していたとしても `go.mod` はルートに1つだけというケースもよく見ます。実際常に最新のバージョンを利用するのであればそれで問題ありませんが、各モジュールを異なるチームで開発していたりするとMulti-module repositoryでモジュール間を疎結合にしておくと便利です。

## tagの打ち方

こちらも公式の通りですが、Multi-module repositoryの場合はルートディレクトリからの相対パスをプレフィックスに付ける必要があります。

以下のようなリポジトリがあったとき:

```
my-repo
|-- .git
|-- go.mod
`-- mig
    |-- go.mod
```

tagは次のようになります。

- `v0.0.1` => my-repo/go.mod
- `mig/v0.0.1` => my-repo/mig/go.mod

この相対パスをプレフィックスに置くという仕様はあまり存在しないのか、10分くらい探したものの既存sermver bumpツールでは適したものを見つけられませんでした。おそらく僕が知らないだけでこの仕様もツールもあると思うので、もしご存知なら教えて下さい。

## takashabe/gumpを使う

gump( https://github.com/takashabe/gump )というツールを使うことでお手軽にMulti-module repositoryスタイルのtagを打てることが出来ます。

```
$ pwd
/home/takashabe/dev/src/github.com/takashabe/actions-exercise/src/deep/deep1
$ gump -g="../../.." --patch -p
✔  create tag src/deep/deep1/v0.0.1
✔  push tag src/deep/deep1/v0.0.1
$ git tag -l | grep src/deep/deep1
src/deep/deep1/v0.0.1
```

## バージョン運用の現場

ここまででMulti-module repositoryでのtagの扱いと、そのためのtagを打つ方法について見てきました。次はこれらを使ってどう開発を回していくかですが、現在僕のチームでは次のポリシーで運用しています。

- 厳密なsemver運用は行わない
- 開発中は手動で `patch` を上げる
- デフォルトブランチにマージしたタイミングで自動で `minor` を上げる

### 厳密なsemver運用は行わない

まずOSSのライブラリなどを開発しているわけではなく、ライブラリなどのバージョンはなるべく最新に追従してくれるクライアントしか存在しない状態です。そのため不意にビルドが出来なくなるなどの懸念を解消出来ればよく、最低限何かしらのバージョンで固定出来ていれば良いと判断しています。

### 開発中は手動で `patch` を上げる

patchが0より大きいものは基本開発中のバージョンとして扱っています。もし依存元と依存先のモジュールをそれぞれ頻繁にbumpしたくなると面倒なので、適宜replaceを使ってる場合もあります。
実際には各モジュールにMakefileを配置しているので `make bump` みたいな感じで動くようにしています。

### デフォルトブランチにマージしたタイミングで自動で `minor` を上げる

モジュールごとの変更がデフォルトブランチ(master)に入ったタイミングでminorを上げて安定版として扱っています。実際に使っているものとは違いますが、以下のような感じでpull-requestがmasterにマージされたときに対象モジュールだけbumpするといったことが実現出来ます。

```yaml:.github/workflows/bump.yaml
on:
  pull_request:
    types:
    - closed

jobs:
  job:
    if: github.event.pull_request.merged == true && github.base_ref == 'master'
...
    - name: bump-version
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      run: |
        pr=$(curl -H "Authorization: token ${GITHUB_TOKEN}" ${{ github.event.pull_request.url }})
        hash=$(echo $pr | jq '.base.sha' | sed 's/\"//g')

        modules=$(hack/list-diff-modules.sh $hash)
        for m in $modules; do
          gump --git-dir . --gomod-dir $m --minor
        done
```

```bash:hack/list-diff-modules.sh
commit=$1
modules=$(find . -type f -name "go.mod")
diffs=()
for m in $modules; do
  m_dir=$(dirname $m)
  m_dir=${m_dir#./}
  result=0
  git diff $commit --quiet --exit-code --relative=$m_dir || result=$?
  if [ $result = 1 ]; then
    diffs+=($m_dir)
  fi
done
echo ${diffs[@]}
```

## まとめ

GoのMulti-module repositoryでのtagの扱いと、それを簡単に扱うために作ったツール( https://github.com/takashabe/gump )について紹介しました。またtakashabe/gumpを使った運用例について紹介しました。
monorepoだけでも事例は少ないですが、Multi-module repositoryの事例もそれ以上に少ないような気がしているので、知見を還元していければと思っています。俺はこんな感じで運用してるぜって話があればぜひ教えてください！