😺

Renovateを用いたBazelにおけるgo_libraryの自動更新

2024/07/23に公開

こんちには。エンジニアのYです。

Sprocketではバックエンドアプリケーションの開発言語の1つとしてGo言語を使用をしています。
Go言語のプロジェクトの依存モジュールはGo Modulesで管理しています。
プロダクトが成長するにつれてモジュールの更新作業を定期的に行うことが難しくなってきました。
そこで、依存するモジュールのバージョンを自動更新するツールとしてRenovateを採用しました。
Go言語のプロジェクトの場合、Renovateは依存モジュールのバージョンアップに合わせてgo.modを更新してプルリクエストを自動で作成してくれます。

Go言語のアプリケーションはBazelにてビルドしています。
バイナリだけではなく、Protocol BuffersからのコードジェネレートとDockerイメージのビルドを行う必要があるためです。

Renovateによってgo.modが更新された場合、Bazelが使用するWORKSPACEに記述されているgo_libraryにその更新を反映する必要があります。
しかし、2021年2月時点ではRenovateはgo_libraryには対応しておりません。

今回はgo.modだけではなくgo_libraryまで含めた依存モジュールを自動更新する手法についてご紹介します。

go_libraryの更新方法

Go Modulesは、go.modとgo.sumにモジュールのバージョンとチェックサムを記載することで、依存モジュールを管理しています。

アプリケーションは、前述のように、Bazelによってビルドしています。
Bazelは、WORKSPACEおよびBUILD.bazel(もしくはBUILD)に記述されているビルドルールに従ってビルドを実行します。
WORKSPACEには外部ワークスペースとの依存関係を、BUILD.bazelにはビルド対象を記述します。
Gazelleにより、go.modに記載されているモジュールの依存関係をgo_libraryとしてWORKSPACEへ追記します。
また、Gazelleは各Goファイルを検出してBUILD.bazelの作成および更新します。

go.modを更新した際は、下記コマンドでGazelleのupdate-reposを実行することでgo.modの内容をgo_libraryに反映させます。

bazel run //:gazelle -- update-repos -from_file=go.mod -prune=true

これにより、go.modの変更に合わせてBazelが使用する依存モジュールのバージョンを更新できます。

Renovateによるgo.modの更新とその問題点

Renovateは、Go言語のみならずその他複数の言語に対応しており、GitHubとの連携も用意されています。
ドキュメントに従ってGitHub Appをインストールし、renovate.jsonという名前の設定ファイルをプロジェクトルートに追加します。
以上の手順を終えると、renovate/というプレフィックスのブランチが切られ、依存モジュールを更新してデフォルトブランチへのプルリクエストを作成してくれるようになります。
しかし、RenovateはWORKSPACEに記述されているgo_libraryの更新は行わず、その更新作業を別途実施する必要があります。

この問題を解決するために、以下の3つの手法を検討しました。

  • すでにCI環境として使用しているCodeBuild Project内でgo_libraryを更新した後、プッシュする。
  • RenovateのpostUpgradeTasksにgo_libraryを更新するタスクを登録する。
  • go_libraryを更新し、プッシュするGiHub Actionsのワークフローを追加する。

CodeBuild Project内でgo_libraryを更新

SprocketではCI環境としてCodeBuildを採用しており、今回対象としているリポジトリもCodeBuild上でテストの実行やアプリケーションのビルドを実行しています。
このCI環境に、go_libraryを更新して変更差分があればチェックアウトブランチにへプッシュするステップを追加する、という方法が考えられます。
しかし、この手法には以下のような問題があります。

  • CodeBuild上でリポジトリへプッシュするためにはGitHub Tokenを安全に管理しておく必要がある。
  • renovate/ブランチ以外ではgo_libraryの更新処理は不要なため、更新をスキップする処理を追加すると、CIのタスク内で分岐が発生する。

RenovateのpostUpgradeTasksにgo_libraryの更新を行うタスクを登録

RenovateにはpostUpgradeTasksという設定があり、バージョンの自動更新後に実行するタスクを定義すると、そのタスクによって更新されたファイル差分をコミットに含めることができます。
このpostUpgradeTasksを使用してgo_libraryを更新すれば良さそうです。
しかしながら、postUpgradeTasksはRenovateをセルフホスティングしている場合のみ有効となる設定です。

つまり、postUpgradeTasksを使ってgo_libraryを自動更新するためには、Renovateを実行するインスタンスを自分で用意しなければなりません。
Kubernetesを用いたRenovateの環境構築の方法は公開されていますが、RenovateをホスティングするためにKubernetesを用意するのは費用対効果が悪いと判断しました。

go_libraryを更新してプッシュするGiHub Actionsのワークフローを追加

最後に、既存のCodeBuildでのCI環境とは別にGitHub Actionsによる新しいワークフローを追加することによりgo_libraryを更新する方法を考えます。
ワークフローを構築するためにはリポジトリ内の.github/workflowsへYAMLファイルを追加します。
ワークフローのコンテキスト情報にGitHub Tokenが存在するため、秘匿情報の管理は不要です。

他の2つの方法と異なり、GitHub Actionsはインフラや秘匿情報の管理コストが発生しません。
以上の理由により、既存のCodeBuildによるCI環境とは別にGitHub Actionsのワークフローを追加することでgo_libraryを更新することを決定しました。

go_libraryをGitHub Actionsで更新

Renovateによるgo.modの更新のgo_libraryへの反映を以下のような方針で実現します。

  1. renovate/というプレフィックスを持つブランチがプッシュされた際にGitHub Actionsのワークフローを起動する。
  2. Gazelleのupdate-reposを実行してgo.modをgo_libraryへ反映させる。
  3. リポジトリ内にファイルの変更差分が存在するかを確認する。
  4. 変更差分がある場合、git addgit commitし、チェックアウトしたブランチにプッシュする。

GitHub Actionsを利用するために、ワークフローを定義したYAMLファイルを.github/workflows内に作成します。
renovate/ブランチのプッシュをトリガーとするには以下のようにYAMLファイルに記述します。

on:
  push:
    branches:
      - "renovate/*

次にgo_libraryを更新します。
go.modの更新に伴い、go.sumに必要のないチェックサムが残っていることがあるので、同時にgo mod tidyも実行します。
GitHub ActionsにはBazelのランチャーであるBazeliskが標準でインストールされています。
よって、USE_BAZEL_VERSIONという環境変数にbazelのバージョンを指定するだけで使用したいバージョンのBazelが実行可能です。
今回使用したBazelのバージョンは3.7.0です。

jobs:
  update-gosum-golibrary:
    runs-on: ubuntu-18.04
    env:
      USE_BAZEL_VERSION: 3.7.0
    steps:

      ...

      - name: Update go.sum and go_library
        run: |
            go mod tidy
            bazel run //:gazelle -- update-repos -from_file=go.mod -prune=true

go.sumもしくはWORKSPACEの変更差分があるかを調べて、差分がある場合はコミットし、プッシュします。
Gitコマンドでも実現可能ですが、変更差分があるかどうかの分岐をテストするため、Go言語にて実装しました。
Gitクライアントとしてgo-gitを使用しています。
以下のようにしてファイルの変更差分が存在するかを取得します。
以降のGo言語のコードでは、冗長な記述を避けるためにエラー処理は省略しています。

repository, err := git.PlainOpenWithOptions(
    "path/to/project/root",
    &git.PlainOpenOptions{DetectDotGit: true},
)

worktree, err := repository.Worktree()

status, err := worktree.Status()

hasChanges := !status.IsClean()

変更分をコミットしてチェックアウトブランチにプッシュするコードは以下のようになります。
Worktreeに明示的に.gitignoreを読み込ませないと無視すべきファイルまでコミットしてしまうので注意が必要です。

worktree.Excludes, err = gitignore.ReadPatterns(worktree.Filesystem, []string{})

err = worktree.AddWithOptions(&git.AddOptions{All: true})

commit, err := worktree.Commit(
    "build(go): update go.sum and go_library automatically",
    &git.CommitOptions{
        Author: &object.Signature{
            Name:  githubUserName,
            Email: githubUserEmail,
            When:  time.Now(),
        },
    })
_, err = repository.CommitObject(commit)

err = repository.Push(&git.PushOptions{
    Auth: &http.BasicAuth{
        Username: githubUserName,
        Password: githubToken,
    },
    Progress: os.Stdout,
})

プッシュするためにはGitHub Tokenが必要ですが、secrets.GITHUB_TOKENというコンテキスト情報があるので環境変数を経由してプログラムに渡すことができます。
GitHub Actionsのワークフローにこれらを実装したコードを実行するステップを追加します。
これでRenovateとGitHub Actionsを使ってgo.modとgo_libraryの両方の同時更新が可能になりました。

go_libraryの更新フロー

最後に、GitHub Actionsのワークフローの定義全体を載せます。

on:
  push:
    branches:
      - "renovate/*

jobs:
  update-gosum-golibrary:
    runs-on: ubuntu-18.04
    env:
      USE_BAZEL_VERSION: 3.7.0
    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Set up Go
        uses: actions/setup-go@v2
        with:
          go-version: "1.15.4"

      - name: Mount bazel cache
        uses: actions/cache@v2
        with:
          path: ~/.cache/bazel
          key: ${{ runner.os }}-${{ hashFiles('WORKSPACE') }}

      - name: Update go.sum and go_library
        run: |
            go mod tidy
            bazel run //:gazelle -- update-repos -from_file=go.mod -prune=true

      - name: Run git commit and push when there are file changes
        run: # ファイルの変更差分があれば add, commit, push するプログラムを実行
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

まとめ

SprocketではGo言語プロジェクトの依存モジュールをRenovateにて自動更新しています。
今回は、Renovateのみでは更新しきれないBazelのビルドファイルの更新をGitHub Actionsで補う手法を紹介しました。
大掛かりなインフラ構築は行わず、かつ秘匿情報の管理コストもかけずにRenovateを実行したいという問題の解決策としてこの手法に行きつきました。

GitHub Actionsは.github/workflowsにYAMLファイルを追加するだけですぐにCIを動かせるので、その他の自動更新ツールへの応用も可能かと思います。

Sprocketで働きませんか?

弊社ではカジュアル面談を実施しております。
ご興味を持たれましたら、こちらからご応募お待ちしております。

Sprocketテックブログ

Discussion