🔄

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

2023/09/13に公開

はじめに

こんにちは、サーバーサイドエンジニアのmynkitです。

サプライチェーンリスク管理クラウドサービスResilireでは、サーバーサイドにGoを採用しています。Goでパッケージを自動更新させたい場合、dependabotやRenovateが選択肢になるかと思います。

ただgRPCなどの自動生成ファイルがあるときはいろいろと厄介で、結論からいうとパッケージをimportするだけのgoファイルを作る必要があります。

今回は自動生成ファイルがある場合に試行錯誤したことと、最終的に作成したdependabot.goの生成方法を共有します。

dependabotと、自動生成ファイルがあるときの課題

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

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

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

とてもありがたいのですが、1つ困ったことがあって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が抜け落ちることがあるようです。(そしてこの問題は対応しない方針のようです)

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というパッケージがあるので、インストールしておきます。

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

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

generate_dependabotgo.pyの作成
# 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に標準パッケージのimportも含まれるので、気持ち悪い方は適宜削除してください。

出力された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がちゃんと機能しているか確認

最後に、dependabotによってgo mod tidyが実行されても問題なさそうか確認しておきましょう。

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

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

おわりに

Resilireでは仲間を募集しています。

サーバーサイドだけでなく、フロントエンドやSREの採用も積極的に行っています。話を聴いてみたい!だけでも良いので、ご興味ある方はぜひご連絡ください!

https://recruit.resilire.jp/for-engineers

Discussion