Golangで、自動生成ファイルがある場合のパッケージ自動更新

Golangでパッケージを自動更新させたい場合、dependabotやrenovateが選択肢になるかと思います。

ただgRPCなどの自動生成ファイルがあるときは、いろいろと厄介だったのでやったことを残しておきます


# 課題

dependabotはパッケージアップデートを自動で行ってくれるGithub純正のBotです。

> Dependabot は、セキュリティアップデートプログラムを使用してプルリクエストを発行することにより、脆弱性のある依存関係を修正できます。

https://docs.github.com/ja/code-security/dependabot/dependabot-security-updates/about-dependabot-security-updates

とてもありがたいのですが、一つ困ったことがあってdependabotが走ってプルリクが出された際、`go mod tidy`が自動で走ります。これによってgo.modに必要なパッケージの追加と不要なパッケージの削除をしてくれるのですが、自動生成ファイルがあるときには悪さをすることがあります。

具体的にいうと、自動生成ファイルのみが呼び出しているパッケージがgo.modから削除されます。そのため自動生成ファイル部分のコードが動かなくなり、Lintなどが落ちます。

# 対応方法

## Renovateの検討(うまくいかなかった)

では`go mod tidy`を自動で走らせなければいいのでは？という気持ちになります。

[Renovate](https://docs.renovatebot.com/)では、dependabotよりもカスタム性のある自動パッケージアップデートができます。また指定しない限りは`go mod tidy`が走らないので今回のケースでも良い選択肢に見えます。

そこで実際に試してみたのですが、自動テスト実行時に

```
go: updates to go.mod needed; to update it:
	go mod tidy
```

と怒られてしまいました。。

Renovateではrenovate.jsonに
```
    "postUpdateOptions": [
        "gomodTidy"
    ],
```
を追記することで`go mod tidy`を実行させることが可能ですが、Renovateの`go mod tidy`はgo.sumからパッケージのsumが抜け落ちることがあるようです。(そしてこの問題は対応しない方針のようです)

https://github.com/renovatebot/renovate/issues/3017

そのため、今回の場合Renovateを使う選択肢はなさそうです。

## dependabot.goの追加(うまくいった)

そもそも今回の問題が報告されていないのか調べてみると、dependabotにissueがありました。

https://github.com/dependabot/dependabot-core/issues/3300

この手法は、自動生成ファイルがつくられるディレクトリにdependabot.goを用意し、自動生成ファイルに用いるパッケージをすべてimportしておく、というものです。

### dependabot.goの生成

すべての生成ファイルを目で確認していくのは大変なので、(なぜかPythonでつくりましたが)dependabot.go生成コードを置いておきます。

#### Install

import部分を認識させるため、ASTを利用します。ASTのjsonをdumpしてくれる[goast](https://github.com/m-mizutani/goast)というパッケージがあるので、インストールしておきます。

```sh
go install github.com/m-mizutani/goast/cmd/goast@latest
```

次にPython3をインストールしておきます。必要な外部パッケージは特にありません。

#### generate_dependabotgo.pyの作成

```python:generate_dependabotgo.py
import subprocess
import json
import glob
import os


def get_package_from_dic(dic):
    if 'Kind' in dic.keys() and dic['Kind']=='ImportSpec':
        return dic['Node']['Path']['Value']
    else:
        return None


def get_imported_packages(go_file):
    cmd = f"goast dump '{go_file}'"

    ast_result = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True).stdout

    dics = [json.loads(l) for l in ast_result.replace('\n}\n{\n', '}\t{').replace('\n', '').replace('\t', '\n').splitlines()]
    
    return set([get_package_from_dic(d) for d in dics if get_package_from_dic(d)])   


def gen_dependabotgofile(dir_path):
    all_packages = []

    if dir_path[-1] == '/':
        dir_path = dir_path[:-1]

    basename = os.path.basename(dir_path)
    go_paths = glob.glob(f'{dir_path}/*.go', recursive=True)
    
    if len(go_paths) == 0:
        Exception(f'dir_path: {dir_path} が適切ではありません')

    for go_path in go_paths:
        all_packages.extend(get_imported_packages(go_path))

    import_txt = ''

    import_txt += f'package {basename}\n'
    import_txt += '\n'
    import_txt += 'import (\n'

    for package in sorted(set(all_packages)):
        import_txt += f'\t_ {package}\n'

    import_txt += ')\n'
    print(import_txt)
    
    with open(f'{dir_path}/dependabot.go', mode='w') as f:
        f.write(import_txt)

def goformat(dir_path):
    subprocess.run(f'goimports -w "{dir_path}/dependabot.go"', shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
        

auto_generated_gofile_dirs = [
    # 自動生成ファイルの作られるディレクトリ達
]

for dir_path in auto_generated_gofile_dirs:
    print(dir_path)
    gen_dependabotgofile(dir_path)
    goformat(dir_path)
```

#### 生成手順

ローカルで実行

1. gRPCなど自動生成ファイルを生成
2. generate_dependabotgo.pyの`auto_generated_gofile_dirs`を書き換えて、`python generate_dependabotgo.py`を実行

出力されたdependabot.goはこんな感じになると思います。dependabot.goがgitの追跡対象になるように.gitignoreの設定も忘れずに。

```go:dependabot.go
package account

import (
	_ "bytes"
	_ "context"
	_ "errors"
	_ "fmt"
	_ "net"
	_ "net/mail"
	_ "net/url"
	_ "reflect"
	_ "regexp"
	_ "sort"
	_ "strings"
	_ "sync"
	_ "time"
	_ "unicode/utf8"

	_ "google.golang.org/grpc"
	_ "google.golang.org/grpc/codes"
	_ "google.golang.org/grpc/status"
	_ "google.golang.org/protobuf/reflect/protoreflect"
	_ "google.golang.org/protobuf/runtime/protoimpl"
	_ "google.golang.org/protobuf/types/known/anypb"
)
```

#### dependabot.goがちゃんと機能しているか確認

1. 自動生成ファイルをすべて削除
2. `go mod tidy`を実行
3. 自動生成ファイルを再度生成
4. TestやLintを実行してエラーがでないか確認

以上で、dependabot.go配置による自動パッケージアッブデート問題は解消します。