Golangでパッケージを自動更新させたい場合、dependabotやrenovateが選択肢になるかと思います。
ただgRPCなどの自動生成ファイルがあるときは、いろいろと厄介だったのでやったことを残しておきます
課題
dependabotはパッケージアップデートを自動で行ってくれるGithub純正のBotです。
Dependabot は、セキュリティアップデートプログラムを使用してプルリクエストを発行することにより、脆弱性のある依存関係を修正できます。
とてもありがたいのですが、一つ困ったことがあってdependabotが走ってプルリクが出された際、go mod tidy
が自動で走ります。これによってgo.modに必要なパッケージの追加と不要なパッケージの削除をしてくれるのですが、自動生成ファイルがあるときには悪さをすることがあります。
具体的にいうと、自動生成ファイルのみが呼び出しているパッケージがgo.modから削除されます。そのため自動生成ファイル部分のコードが動かなくなり、Lintなどが落ちます。
対応方法
Renovateの検討(うまくいかなかった)
ではgo mod tidy
を自動で走らせなければいいのでは?という気持ちになります。
Renovateでは、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が抜け落ちることがあるようです。(そしてこの問題は対応しない方針のようです)
そのため、今回の場合Renovateを使う選択肢はなさそうです。
dependabot.goの追加(うまくいった)
そもそも今回の問題が報告されていないのか調べてみると、dependabotにissueがありました。
この手法は、自動生成ファイルがつくられるディレクトリにdependabot.goを用意し、自動生成ファイルに用いるパッケージをすべてimportしておく、というものです。
dependabot.goの生成
すべての生成ファイルを目で確認していくのは大変なので、(なぜかPythonでつくりましたが)dependabot.go生成コードを置いておきます。
Install
import部分を認識させるため、ASTを利用します。ASTのjsonをdumpしてくれるgoastというパッケージがあるので、インストールしておきます。
go install github.com/m-mizutani/goast/cmd/goast@latest
次にPython3をインストールしておきます。必要な外部パッケージは特にありません。
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)
生成手順
ローカルで実行
- gRPCなど自動生成ファイルを生成
- generate_dependabotgo.pyの
auto_generated_gofile_dirs
を書き換えて、python generate_dependabotgo.py
を実行
出力されたdependabot.goはこんな感じになると思います。dependabot.goがgitの追跡対象になるように.gitignoreの設定も忘れずに。
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がちゃんと機能しているか確認
- 自動生成ファイルをすべて削除
-
go mod tidy
を実行 - 自動生成ファイルを再度生成
- TestやLintを実行してエラーがでないか確認
以上で、dependabot.go配置による自動パッケージアッブデート問題は解消します。