Diary of a Perpetual Student

Perpetual Student: A person who remains at university far beyond the normal period

GitHub ActionsのGoのバージョンをtoolchainディレクティブの指定ぴったりで固定したい場合

blog.arthur1.dev

以前公開したGoのバージョン指定・更新に関するブログで、GitHub Actionsのsetup-goについてこんなことを書きました。

stableを指定しておけば、大体の場合(最新に保った)toolchainディレクティブ以上のバイナリを最初から用意してくれるでしょう。

「以上」というのは、toolchainディレクティブで指定したものより新しいバージョンのGoがインストールされている時に、指定したバージョンまでツールチェーンのバージョンを後退されることはないということです。

Go言語はバージョン1.xの間後方互換性を壊さないつもりであると計画されて開発されています*1。多くの場合、Goのバージョンが指定したものより新しいからといって困ることはないでしょう。

さて、それでも意図せずにGoのバージョンが上がるのを防ぎ、宣言した通りに制御したいケースというのはやはり存在します。たとえば、セキュリティ対応のため、Go言語のアップデートに伴い今まで使えていた暗号スイートがデフォルトの設定からなくなることがあります。このような場合ではコードをそのままにした状態での後方互換性は失われてしまっているため、意図せずGoのバージョンが上がると、アプリケーションが脆弱な暗号スイートを用いるエンドポイントと通信できなくなる障害が発生することになるかもしれません。

Goのバージョンはコードで厳密に管理したいけど、toolchainディレクティブに対応していないsetup-goにわざわざ個別にgo-versionを指定してメンテナンスするのは大変だという方のために、先ほどのエントリではワークアラウンドを紹介しています。それは、setup-goのバージョンを1.21に固定してしまうという方法です。ツールチェーンの仕組みで新しいGoのバージョンが利用できるため、できる限り古くしておけば常にtoolchainディレクティブで指定したGoが利用できるという理屈です。そして、ツールチェーンの仕組みが入ったのがGo 1.21からなので1.21を指定するというわけですね。

さて、この方法は正直美しいとは言えませんし、指定したツールチェーンをダウンロードして利用可能にするオーバヘッドが生じます。そこで、このエントリでは別のアプローチを提案しましょう。それは、先にgo.modのtoolchainディレクティブを解析して、一致するバージョンをsetup-goにgo-versionに渡すという方法です。

こんなものを作ってみました。

github.com

これはgo.modファイル(やgo.workファイル)を解析し、goディレクティブやtoolchainディレクティブの値をOutputに書き出してくれるactionです。

以下のようにしてidをつけてArthur1/parse-gomod-actionを呼び出し、toolchain-go-versionというoutputをsetup-goのgo-versionに渡してあげます。すると、go.modのtoolchain directiveで指定したバージョンのGoが利用可能になります。

- id: gomod
  uses: Arthur1/parse-gomod-action@v0
- uses: actions/setup-go@v5
  with:
    go-version: ${{ steps.gomod.outputs.toolchain-go-version }}

もし、toolchainディレクティブを記述していないリポジトリでも同様の記述に揃えたい、かつそのケースではsetup-goにgo-version-fileを指定したときの挙動(goディレクティブの宣言と一致するバージョンのGoを利用可能にする)をしてほしい場合には、以下のように記述すると良いでしょう。

go-version: ${{ steps.gomod.outputs.toolchain-go-version || steps.gomod.outputs.go }}

2024-09-13追記: DockerでGoをビルドするステージでも同様に固定したい場合

GitHub Actionsの話と同様に、DockerfileでもTOOLCHAIN=autoに頼ってタグを意図的に古くしておくことでtoolchainディレクティブの宣言通りにすることは可能です。しかし、そうしたくない場合にはやはり外部からバージョンを指定してあげるやり方が良いのではないかと思います。

以下のようにARGを用いてgolangイメージのタグを指定できるようにします。

ARG GO_BUILD_IMAGE_TAG="latest"

FROM golang:${GO_BUILD_IMAGE_TAG} AS builder

go build ./main.go

...

あとは、GitHub Actionsでdocker buildする際に、parse-gomod-actionから得たtoolchain-go-versionからGO_BUILD_IMAGE_TAG ARGを生成し、docker buildの--build-arg(docker/build-push-actionを利用する場合にはbuild-args)に渡してあげます。すると、toolchainディレクティブで宣言したGoのバージョンの対応するイメージを使ってDocker上でGoのビルドをすることができます。

- id: gomod
  uses: Arthur1/parse-gomod-action@v0
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v6
  with:
    context: .
    build-args:
      - GO_BUILD_IMAGE_TAG=${steps.gomod.outputs.toolchain-go-version}-bookworm

この手法ではGOTOOLCHAIN=autoを有効にした場合の必要なツールチェーンのロードが発生しないため、最初に紹介した方法と比較してパフォーマンス面では優れているでしょう。